pubid 2.0.0.pre.alpha.11 → 2.0.0.pre.alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.adoc +43 -1
- data/data/ieee/update_codes.yaml +17 -4
- data/data/nist/update_codes.yaml +7 -3
- data/lib/pubid/adobe/builder.rb +2 -0
- data/lib/pubid/all_parts.rb +201 -0
- data/lib/pubid/all_parts_identifier.rb +19 -0
- data/lib/pubid/amca/CLAUDE.md +47 -0
- data/lib/pubid/amca/builder.rb +3 -5
- data/lib/pubid/amca/identifiers/base.rb +10 -0
- data/lib/pubid/amca/identifiers/publication.rb +13 -0
- data/lib/pubid/amca/parser.rb +2 -1
- data/lib/pubid/amca/renderer.rb +22 -33
- data/lib/pubid/amca/urn_generator.rb +21 -2
- data/lib/pubid/amca/urn_parser.rb +36 -10
- data/lib/pubid/ansi/builder.rb +6 -0
- data/lib/pubid/api/CLAUDE.md +23 -0
- data/lib/pubid/api/builder.rb +2 -0
- data/lib/pubid/ashrae/CLAUDE.md +13 -0
- data/lib/pubid/ashrae/builder.rb +58 -14
- data/lib/pubid/ashrae/identifiers/errata.rb +14 -2
- data/lib/pubid/ashrae/identifiers/interpretation.rb +2 -10
- data/lib/pubid/ashrae/parser.rb +62 -28
- data/lib/pubid/ashrae/renderer.rb +32 -1
- data/lib/pubid/ashrae/urn_generator.rb +32 -9
- data/lib/pubid/asme/CLAUDE.md +25 -0
- data/lib/pubid/asme/builder.rb +16 -9
- data/lib/pubid/asme/components/code.rb +2 -0
- data/lib/pubid/asme/identifiers/standard.rb +6 -1
- data/lib/pubid/asme/parser.rb +41 -14
- data/lib/pubid/astm/CLAUDE.md +9 -0
- data/lib/pubid/astm/builder.rb +2 -0
- data/lib/pubid/astm/components/code.rb +2 -0
- data/lib/pubid/astm/parser.rb +4 -1
- data/lib/pubid/bipm/CLAUDE.md +11 -0
- data/lib/pubid/bipm/builder.rb +2 -0
- data/lib/pubid/bsi/CLAUDE.md +93 -0
- data/lib/pubid/bsi/builder.rb +13 -11
- data/lib/pubid/bsi/components/date.rb +10 -4
- data/lib/pubid/bsi/identifiers/addendum_document.rb +2 -0
- data/lib/pubid/bsi/identifiers/adopted_european_norm.rb +6 -54
- data/lib/pubid/bsi/identifiers/adopted_international_standard.rb +5 -22
- data/lib/pubid/bsi/identifiers/amendment.rb +36 -12
- data/lib/pubid/bsi/identifiers/british_industrial_practice.rb +1 -1
- data/lib/pubid/bsi/identifiers/bundled_identifier.rb +2 -0
- data/lib/pubid/bsi/identifiers/consolidated_identifier.rb +23 -26
- data/lib/pubid/bsi/identifiers/corrigendum.rb +29 -12
- data/lib/pubid/bsi/identifiers/expert_commentary.rb +6 -7
- data/lib/pubid/bsi/identifiers/handbook.rb +1 -1
- data/lib/pubid/bsi/identifiers/national_annex.rb +18 -20
- data/lib/pubid/bsi/identifiers/practice_guide.rb +1 -1
- data/lib/pubid/bsi/identifiers/root_identity.rb +31 -0
- data/lib/pubid/bsi/identifiers/set.rb +2 -0
- data/lib/pubid/bsi/identifiers/supplement_document.rb +2 -0
- data/lib/pubid/bsi/identifiers.rb +1 -0
- data/lib/pubid/bsi/parser.rb +8 -8
- data/lib/pubid/bsi/renderer.rb +20 -20
- data/lib/pubid/bsi/single_identifier.rb +11 -2
- data/lib/pubid/bsi/urn_generator.rb +28 -18
- data/lib/pubid/builder/base.rb +27 -0
- data/lib/pubid/calconnect/builder.rb +2 -0
- data/lib/pubid/calconnect/identifier.rb +4 -0
- data/lib/pubid/ccsds/builder.rb +2 -0
- data/lib/pubid/ccsds/identifier.rb +12 -0
- data/lib/pubid/ccsds/single_identifier.rb +14 -10
- data/lib/pubid/cen_cenelec/CLAUDE.md +59 -0
- data/lib/pubid/cen_cenelec/builder.rb +6 -1
- data/lib/pubid/cen_cenelec/identifier.rb +10 -27
- data/lib/pubid/cen_cenelec/identifiers/amendment.rb +3 -10
- data/lib/pubid/cen_cenelec/identifiers/corrigendum.rb +3 -10
- data/lib/pubid/cen_cenelec/parser.rb +11 -3
- data/lib/pubid/cie/CLAUDE.md +58 -0
- data/lib/pubid/cie/builder.rb +2 -0
- data/lib/pubid/cie/components/language.rb +2 -0
- data/lib/pubid/components/adoption.rb +2 -0
- data/lib/pubid/components/code.rb +2 -0
- data/lib/pubid/components/date.rb +8 -6
- data/lib/pubid/components/edition.rb +2 -0
- data/lib/pubid/components/iteration.rb +2 -0
- data/lib/pubid/components/language.rb +2 -0
- data/lib/pubid/components/locality.rb +2 -0
- data/lib/pubid/components/publisher.rb +2 -0
- data/lib/pubid/components/relationship.rb +2 -0
- data/lib/pubid/components/stage.rb +2 -0
- data/lib/pubid/components/supplement.rb +2 -0
- data/lib/pubid/components/type.rb +2 -0
- data/lib/pubid/components/typed_stage.rb +8 -0
- data/lib/pubid/csa/CLAUDE.md +41 -0
- data/lib/pubid/csa/builder.rb +5 -3
- data/lib/pubid/csa/identifier.rb +13 -1
- data/lib/pubid/csa/identifiers/bundled.rb +2 -2
- data/lib/pubid/csa/identifiers/cec.rb +1 -1
- data/lib/pubid/csa/renderer.rb +12 -12
- data/lib/pubid/csa/single_identifier.rb +18 -1
- data/lib/pubid/csa/urn_generator.rb +1 -1
- data/lib/pubid/doi/builder.rb +2 -0
- data/lib/pubid/easc/builder.rb +2 -0
- data/lib/pubid/ecma/CLAUDE.md +28 -0
- data/lib/pubid/ecma/builder.rb +2 -0
- data/lib/pubid/ecma/identifier.rb +7 -0
- data/lib/pubid/etsi/CLAUDE.md +34 -0
- data/lib/pubid/etsi/builder.rb +2 -0
- data/lib/pubid/etsi/components/code.rb +6 -0
- data/lib/pubid/etsi/components/version.rb +2 -0
- data/lib/pubid/etsi/identifiers/etsi_standard.rb +7 -0
- data/lib/pubid/evs/CLAUDE.md +58 -0
- data/lib/pubid/evs/builder.rb +3 -1
- data/lib/pubid/evs/identifier.rb +6 -6
- data/lib/pubid/evs/identifiers/national_adoption.rb +7 -2
- data/lib/pubid/evs/renderer.rb +1 -1
- data/lib/pubid/evs/urn_generator.rb +1 -1
- data/lib/pubid/evs/urn_parser.rb +1 -1
- data/lib/pubid/gb/CLAUDE.md +140 -0
- data/lib/pubid/gb/builder.rb +21 -11
- data/lib/pubid/gb/identifier.rb +28 -7
- data/lib/pubid/gb/identifiers/all_parts.rb +17 -0
- data/lib/pubid/gb/identifiers.rb +1 -0
- data/lib/pubid/gb/parser.rb +4 -0
- data/lib/pubid/gb/renderer.rb +4 -4
- data/lib/pubid/gb.rb +1 -0
- data/lib/pubid/gost/CLAUDE.md +64 -0
- data/lib/pubid/gost/builder.rb +3 -1
- data/lib/pubid/gost/identifier.rb +5 -0
- data/lib/pubid/iala/CLAUDE.md +82 -0
- data/lib/pubid/iala/builder.rb +2 -0
- data/lib/pubid/iana/CLAUDE.md +7 -0
- data/lib/pubid/iana/builder.rb +2 -0
- data/lib/pubid/identifier.rb +172 -20
- data/lib/pubid/idf/builder.rb +6 -1
- data/lib/pubid/idf/identifier.rb +12 -0
- data/lib/pubid/idf/identifiers/all_parts.rb +17 -0
- data/lib/pubid/idf/identifiers/amendment.rb +1 -1
- data/lib/pubid/idf/identifiers/corrigendum.rb +1 -1
- data/lib/pubid/idf/identifiers/international_standard.rb +1 -1
- data/lib/pubid/idf/identifiers/reviewed_method.rb +1 -1
- data/lib/pubid/idf/identifiers.rb +1 -0
- data/lib/pubid/idf/single_identifier.rb +1 -1
- data/lib/pubid/iec/CLAUDE.md +31 -0
- data/lib/pubid/iec/builder.rb +7 -1
- data/lib/pubid/iec/components/consolidated_amendment.rb +4 -0
- data/lib/pubid/iec/components/sheet.rb +2 -0
- data/lib/pubid/iec/components/trf_info.rb +2 -0
- data/lib/pubid/iec/components/vap_suffix.rb +2 -0
- data/lib/pubid/iec/identifier.rb +7 -2
- data/lib/pubid/iec/identifiers/all_parts.rb +19 -0
- data/lib/pubid/iec/identifiers.rb +1 -0
- data/lib/pubid/iec/renderer.rb +0 -1
- data/lib/pubid/iec/urn_generator.rb +9 -1
- data/lib/pubid/iec/urn_parser.rb +3 -2
- data/lib/pubid/ieee/CLAUDE.md +97 -0
- data/lib/pubid/ieee/builder.rb +176 -7
- data/lib/pubid/ieee/components/code.rb +2 -0
- data/lib/pubid/ieee/components/draft.rb +35 -2
- data/lib/pubid/ieee/components/typed_stage.rb +2 -0
- data/lib/pubid/ieee/identifiers/base.rb +20 -0
- data/lib/pubid/ieee/identifiers/iec_ieee_copublished.rb +9 -0
- data/lib/pubid/ieee/identifiers/joint_development.rb +17 -10
- data/lib/pubid/ieee/identifiers/project_draft_identifier.rb +8 -1
- data/lib/pubid/ieee/identifiers/si_standard.rb +5 -2
- data/lib/pubid/ieee/ire/parser.rb +19 -1
- data/lib/pubid/ieee/parser.rb +337 -49
- data/lib/pubid/ieee/project_renderer.rb +46 -0
- data/lib/pubid/ieee/renderer.rb +46 -14
- data/lib/pubid/ieee/urn_generator.rb +31 -0
- data/lib/pubid/ieee.rb +2 -0
- data/lib/pubid/ietf/CLAUDE.md +7 -0
- data/lib/pubid/ietf/builder.rb +2 -0
- data/lib/pubid/iho/builder.rb +2 -0
- data/lib/pubid/isbn/builder.rb +2 -0
- data/lib/pubid/iso/CLAUDE.md +47 -0
- data/lib/pubid/iso/builder.rb +29 -16
- data/lib/pubid/iso/components/publisher.rb +2 -0
- data/lib/pubid/iso/components.rb +0 -1
- data/lib/pubid/iso/identifier.rb +17 -18
- data/lib/pubid/iso/identifiers/all_parts.rb +19 -0
- data/lib/pubid/iso/identifiers/directives.rb +7 -4
- data/lib/pubid/iso/identifiers/directives_supplement.rb +4 -2
- data/lib/pubid/iso/identifiers/tc_document.rb +31 -46
- data/lib/pubid/iso/identifiers.rb +1 -0
- data/lib/pubid/iso/normalizer.rb +1 -1
- data/lib/pubid/iso/rendering_style.rb +0 -1
- data/lib/pubid/iso/urn_generator.rb +10 -10
- data/lib/pubid/iso.rb +5 -4
- data/lib/pubid/itu/CLAUDE.md +69 -0
- data/lib/pubid/itu/builder.rb +12 -0
- data/lib/pubid/itu/components/code.rb +2 -0
- data/lib/pubid/itu/components/designation.rb +2 -0
- data/lib/pubid/itu/components/sector.rb +2 -0
- data/lib/pubid/itu/components/series.rb +2 -0
- data/lib/pubid/itu/identifiers/base.rb +10 -0
- data/lib/pubid/itu/identifiers/contribution.rb +31 -0
- data/lib/pubid/itu/identifiers/supplement.rb +15 -0
- data/lib/pubid/itu/identifiers.rb +1 -0
- data/lib/pubid/itu/parser.rb +19 -2
- data/lib/pubid/jcgm/CLAUDE.md +7 -0
- data/lib/pubid/jcgm/builder.rb +2 -0
- data/lib/pubid/jcgm/components/publisher.rb +2 -0
- data/lib/pubid/jis/builder.rb +5 -1
- data/lib/pubid/jis/identifier.rb +5 -17
- data/lib/pubid/jis/identifiers/all_parts.rb +19 -0
- data/lib/pubid/jis/identifiers.rb +1 -0
- data/lib/pubid/jis/renderer.rb +0 -2
- data/lib/pubid/jis/urn_generator.rb +0 -1
- data/lib/pubid/nist/CLAUDE.md +56 -0
- data/lib/pubid/nist/builder.rb +14 -12
- data/lib/pubid/nist/components/edition.rb +2 -0
- data/lib/pubid/nist/components/issue_number.rb +2 -0
- data/lib/pubid/nist/components/part.rb +2 -0
- data/lib/pubid/nist/components/stage.rb +2 -0
- data/lib/pubid/nist/components/supplement.rb +2 -0
- data/lib/pubid/nist/components/translation.rb +2 -0
- data/lib/pubid/nist/components/update.rb +2 -0
- data/lib/pubid/nist/components/version.rb +2 -0
- data/lib/pubid/nist/components/volume.rb +2 -0
- data/lib/pubid/nist/identifiers/base.rb +34 -6
- data/lib/pubid/nist/identifiers/commercial_standard_emergency.rb +1 -1
- data/lib/pubid/nist/identifiers/crpl_report.rb +5 -6
- data/lib/pubid/nist/identifiers/federal_information_processing_standards.rb +2 -2
- data/lib/pubid/nist/identifiers/miscellaneous_publication.rb +2 -2
- data/lib/pubid/nist/parser.rb +15 -2
- data/lib/pubid/nist/preprocessor.rb +53 -2
- data/lib/pubid/nist/series/ir.rb +3 -7
- data/lib/pubid/oasis/CLAUDE.md +19 -0
- data/lib/pubid/oasis/builder.rb +2 -0
- data/lib/pubid/oasis/identifier.rb +19 -0
- data/lib/pubid/ogc/CLAUDE.md +34 -0
- data/lib/pubid/ogc/builder.rb +2 -0
- data/lib/pubid/ogc/identifier.rb +11 -0
- data/lib/pubid/oiml/CLAUDE.md +189 -0
- data/lib/pubid/oiml/builder.rb +22 -2
- data/lib/pubid/oiml/components/code.rb +6 -0
- data/lib/pubid/oiml/identifier.rb +13 -0
- data/lib/pubid/oiml/identifiers/annex.rb +4 -0
- data/lib/pubid/oiml/identifiers/bulletin.rb +30 -36
- data/lib/pubid/oiml/identifiers/certification_system.rb +34 -0
- data/lib/pubid/oiml/identifiers/code_number.rb +10 -2
- data/lib/pubid/oiml/identifiers/dual_published.rb +174 -0
- data/lib/pubid/oiml/identifiers.rb +2 -0
- data/lib/pubid/oiml/parser.rb +41 -5
- data/lib/pubid/oiml/renderer.rb +26 -4
- data/lib/pubid/oiml/single_identifier.rb +5 -1
- data/lib/pubid/oiml/supplement_identifier.rb +7 -0
- data/lib/pubid/oiml/urn_generator.rb +32 -4
- data/lib/pubid/oiml.rb +5 -0
- data/lib/pubid/omg/CLAUDE.md +15 -0
- data/lib/pubid/omg/builder.rb +2 -0
- data/lib/pubid/parser/grammar.rb +23 -0
- data/lib/pubid/plateau/builder.rb +2 -0
- data/lib/pubid/plateau/identifiers/base.rb +4 -0
- data/lib/pubid/plateau/supplement_identifier.rb +14 -2
- data/lib/pubid/plateau/urn_generator.rb +7 -1
- data/lib/pubid/renderers/directives_renderer.rb +3 -1
- data/lib/pubid/renderers/human_readable.rb +0 -1
- data/lib/pubid/sae/builder.rb +3 -1
- data/lib/pubid/sae/components/date.rb +2 -0
- data/lib/pubid/sae/components/type.rb +2 -0
- data/lib/pubid/sae/identifiers/base.rb +4 -1
- data/lib/pubid/sae/urn_generator.rb +1 -1
- data/lib/pubid/subset_match.rb +197 -0
- data/lib/pubid/tgpp/CLAUDE.md +43 -0
- data/lib/pubid/tgpp/builder.rb +2 -0
- data/lib/pubid/tgpp/identifier.rb +14 -0
- data/lib/pubid/type_resolver.rb +14 -2
- data/lib/pubid/un/builder.rb +2 -0
- data/lib/pubid/version.rb +1 -1
- data/lib/pubid/w3c/CLAUDE.md +7 -0
- data/lib/pubid/w3c/builder.rb +2 -0
- data/lib/pubid/xsf/CLAUDE.md +11 -0
- data/lib/pubid/xsf/builder.rb +2 -0
- data/lib/pubid.rb +16 -3
- data/lib/tasks/docs.rake +13 -11
- metadata +46 -4
- data/lib/pubid/ccsds/identifiers/base_BASE_88929.rb +0 -70
- data/lib/pubid/iso/components/code.rb +0 -24
|
@@ -9,6 +9,8 @@ module Pubid
|
|
|
9
9
|
# Human render: abbreviation with flavor-specific separator.
|
|
10
10
|
# URN render: stage abbreviation alone (no separator).
|
|
11
11
|
class Stage < Lutaml::Model::Serializable
|
|
12
|
+
include ::Pubid::SubsetMatch
|
|
13
|
+
|
|
12
14
|
attribute :name, :string
|
|
13
15
|
attribute :stage_code, :string
|
|
14
16
|
attribute :harmonized_stages, :string, collection: true
|
|
@@ -26,6 +26,8 @@ module Pubid
|
|
|
26
26
|
# order they appear in the printed supplement; the URN form joins number
|
|
27
27
|
# and year with ":".
|
|
28
28
|
class Supplement < Lutaml::Model::Serializable
|
|
29
|
+
include ::Pubid::SubsetMatch
|
|
30
|
+
|
|
29
31
|
DEFAULT_TYPE = "sup"
|
|
30
32
|
|
|
31
33
|
attribute :type, :string, default: -> { DEFAULT_TYPE }
|
|
@@ -9,6 +9,8 @@ module Pubid
|
|
|
9
9
|
# Human render: abbreviation with flavor-specific separator.
|
|
10
10
|
# URN render: type code (e.g. "tr", "ts") per RFC 5141-bis.
|
|
11
11
|
class Type < Lutaml::Model::Serializable
|
|
12
|
+
include ::Pubid::SubsetMatch
|
|
13
|
+
|
|
12
14
|
attribute :name, :string
|
|
13
15
|
attribute :abbr, :string
|
|
14
16
|
attribute :type_code, :string
|
|
@@ -7,6 +7,14 @@ module Pubid
|
|
|
7
7
|
# Human render: typed-stage abbreviation with flavor-specific separator.
|
|
8
8
|
# URN render: stage code (RFC 5141-bis: stage-XX.XX format handled by urn_generator).
|
|
9
9
|
class TypedStage < Lutaml::Model::Serializable
|
|
10
|
+
include ::Pubid::SubsetMatch
|
|
11
|
+
|
|
12
|
+
# `original_abbr` records the input spelling ("Amd" or "AMD"), not the
|
|
13
|
+
# stage, so a subset match skips it.
|
|
14
|
+
def self.subset_ignored_attributes
|
|
15
|
+
%i[original_abbr]
|
|
16
|
+
end
|
|
17
|
+
|
|
10
18
|
attribute :name, :string
|
|
11
19
|
attribute :code, :string
|
|
12
20
|
attribute :type_code, :string
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# CSA flavor notes
|
|
2
|
+
|
|
3
|
+
CSA convergence onto the cross-flavor identifier contract.
|
|
4
|
+
|
|
5
|
+
These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/csa/` or `spec/pubid/csa/`. The root file keeps the cross-flavor contract that every flavor obeys.
|
|
6
|
+
|
|
7
|
+
- **CSA converged onto the cross-flavor contract (`root.number`, real container types, raise-never-nil)**: CSA was the last flavor outside the identifier contract, and the only one where `id.root.number` — the key `Relaton::Index::Type#candidates_by_number` sorts and bsearches on — **raised `NoMethodError`** rather than returning a value. A raise aborts a crawl; an empty key only degrades a search. Measured on `main` over the 829 parseable fixture ids: **0** had a non-empty `root.number`, **600** raised, only 228 survived `from_hash(to_hash)`, 13 raised on `to_mr_string` and 601 of 816 slugs carried characters outside `[a-z0-9._-]`. Four independent changes, plus three fixes they exposed.
|
|
8
|
+
**(1) `code` → `number`, and why it needed no type override.** `SingleIdentifier` declared `attribute :code, Components::Code` and never set the `number` it inherits; the fix **deletes the duplicate** rather than redeclaring anything, because `Pubid::Csa::Components::Code` **is** a bare alias of `Pubid::Components::Code` — exactly the type `::Pubid::Identifier` already declares for `number`. That distinction is load-bearing: CSA's base **is** split across two files (`csa/identifier.rb` + `csa/single_identifier.rb`), the IEEE counter-shape in which a leaf can snapshot a half-built parent attribute table (the `number` determinism landmine), so a *redeclaration* here would have been unsafe where W3C's one-file base made it safe. There is nothing to resolve nondeterministically when you declare nothing. `code` is dropped with **no alias**; readers moved in `builder.rb`, `renderer.rb`, `urn_generator.rb`, `bundled.rb`, `combined.rb`. **The serialized shape is flat** — a `key_value` block on `SingleIdentifier` collapses the `Components::Code` attributes to bare scalars (`number: C22.2-286`, not `number: {value: C22.2-286}`), the ISO/ETSI/ITU pattern. That is a *serialization mapping*, not a retype: the runtime attribute stays a `Components::Code`, so the determinism landmine is still avoided. Two placement facts matter — a lutaml `key_value` block is **exhaustive** (an unmapped attribute is silently dropped, `_type` included, so every attribute must be listed), and the block sits on **`SingleIdentifier`**, which only the single-document types inherit; the containers descend from `Csa::Identifier` directly and keep the default shape for their nested `base`/`identifiers` (whose members still flatten, via lutaml's nested transform). `Cec` declares a **second** block for `cec_part`, which lutaml merges with the inherited one; its `no_number` converter reads through either a `:string` (SingleIdentifier) or a `Components::Code` (Cec), since Cec redeclares that attribute. `Identifiers::Cec#code` (a *method* synthesising `"#{cec_part.value}-#{no_number.value}"`) became `def number` — it shadows lutaml's generated reader, safe because `Cec` declares no `number` of its own (the ITU `itu/identifiers/base.rb` precedent), and lossless because `cec_part`/`no_number` are themselves serialized so `from_hash` re-synthesises the same value. Keys: `Standard` → `"B149.1"`, `Series` → `"Z240"`, `Cec` → `"C22.2-286"`.
|
|
9
|
+
**(2) The five containers became real identifiers.** `WrapperIdentifier`, `CompositeIdentifier`, `Bundled` and `Combined` descended from bare `Lutaml::Model::Serializable` with a hand-written `def root; self; end`, and held their parent link in an **`attr_accessor`** — invisible to `to_hash`, `from_hash` **and** `#exclude` (which iterates `self.class.attributes`), so `CanadianAdopted#to_hash` was `{"reaffirmation" => "2023"}` and `CsaAdopted#to_hash` was `{}`. All four now `< Pubid::Csa::Identifier` with `attribute :base, ::Pubid::Identifier, polymorphic: true` — the **uniform `base` name**, so the inherited `#root` walks it and `exclude_from_nested` reaches inside; `wrapped_identifier` is gone with **no alias**. The type must be the **cross-flavor** `::Pubid::Identifier`, because `CsaAdopted` wraps ISO/IEC/CISPR ids and `polymorphic: true` widens only to *subclasses* (the `Ieee::Identifiers::AdoptedStandard` precedent); `Bundled#base`/`#bundled_with` were widened for the same reason, since a bundle's base is routinely a `Cec`, which is not a `Standard`. **`Combined` collapsed its `first`/`second`/`third` triple into one `identifiers` collection** (the IEC `ConsolidatedIdentifier` shape) — the triple was an observable hash shape hard-capped at three, and there is no published CSA index to migrate, so it was free to move; `#root` walks `identifiers.first`. All nine `polymorphic_name`s derive correctly and distinctly (`pubid:csa:canadian-adopted`, …), so **no explicit override is needed** — verify against `Pubid::Csa::Identifier.polymorphic_type_map` before adding one. The duplicated `to_mr_string`/`to_slug` on the two container base classes were deleted; the containers inherit `Csa::Identifier`'s, which also ended 13 `NoMethodError`s on `Combined`/`Bundled`. **`Identifiers::Base` stays a real intermediate class** (`< SingleIdentifier`), so CSA remains Category B in `identifier_hierarchy_spec.rb` — the containers attach to `Csa::Identifier` and deliberately bypass it.
|
|
10
|
+
**(3) The MR slug is charset-filtered, not escape-listed.** `to_mr_string` keeps its three *semantic* mappings (` ` → `.`, `:` → `.`, `/` → `-`, which carry CSA's segment structure) and then neutralises everything else by charset (`gsub(/[^a-z0-9._-]+/, "-")`, trimming leading/trailing separators) — the BIPM `mr_slug` precedent. Real CSA references carry `(`, `)`, `,`, `&` and `+`, all of which the old `tr` chain let through into what `to_slug` hands a filesystem. CSA keeps its whole-string `to_mr_string` override rather than moving to the `Renderers::MrString` hooks, because CSA identity lives in a dozen shape-specific attributes those seven hooks cannot see and `to_s` already round-trips losslessly. Still 0 collisions.
|
|
11
|
+
**(4) `parse` raises, never returns `nil`.** CSA and `api` were the only two flavors whose `parse` could return `nil` (measured across all 44 registered flavors; 28 raise a bare `RuntimeError`, 15 `Parslet::ParseFailed`, `bsi` a bare `StandardError` — the gem still has no single designed failure type, which is a **live follow-up**). A nil surfaces in the caller as a `NoMethodError` far from the input that caused it. All eight `return nil` branches now raise `Parslet::ParseFailed`, via a `build!` helper for the two that returned whatever `Builder#build` gave them. The guards after a *recursive* `parse` are simply gone — the inner call raises now. Two internal nil signals are deliberately kept: `parse_external_standard` (it probes several flavors) is translated to a raise at its one call site, and the package base scan — which walks progressively longer prefixes and keeps the longest that parses — gets a private **`try_parse`** that rescues back to nil.
|
|
12
|
+
**Three fixes these exposed.** **(a)** The builder marked a prefix-less reference (`C22.1-15`) with `publisher_prefix = ""`, a sentinel the canonical `to_hash` **drops by design** (it strips empty and default-valued attributes) — so such an id came back from `from_hash` with a nil prefix and re-rendered as `CSA C22.1-15`. Replaced by `attribute :code_only, :boolean, default: -> { false }`, named for the RARE case per the ITU rule so it is dropped from every ordinary row, read through `Renderer#publisher_prefix_for` and `UrnGenerator#code_only?` (the URN keeps its **empty** publisher segment, so URNs stay byte-identical). **(b)** `builder.rb` carried three `rescue Pubid::Errors::ParseError` clauses — **that constant does not exist**, only `Pubid::UrnParser::Errors::ParseError` does, so those "try ISO, else IEC" fallbacks raised `NameError` instead of falling back. Same class as the IETF URN-parser bug already recorded in `lib/pubid/ietf/CLAUDE.md`. **(c)** **Known, deliberately untouched:** `spec/pubid/csa/fixtures_spec.rb`'s glob has one `..` too many (repo-root `fixtures/`), so the whole fixture round-trip spec silently iterates an empty file list and reports **0 examples** — the exact ITU defect, again. It is left alone here because fixing it forces a `rake "validation:classify[csa]"` run, and that generator rewrites *whole files*: correcting the one bucket this change invalidates (`pass/nil_class.txt`, whose 21 entries now raise instead of returning nil) mechanically drags in ~28 unrelated `fail` → `pass` corrections that have been stale since 2026-04-01. Keeping them out keeps this diff to the task. Consequence to know: `pass/nil_class.txt` still lists 21 ids as "nil class" that now raise — inert, because nothing reads it while the glob is broken, and because CSA (unlike calconnect/bipm/ogc/iana) has no spec asserting that `fail/` fixtures fail. (follow-up hand-off: csa-revive-fixture-net.)
|
|
13
|
+
**The containers also needed the wrapper-specific overrides, not just the parent link.** Making them identifiers wires up `to_hash`/`from_hash`/`#exclude`/`root.number` for free, but `#base_document` and `#drop_supplements` — the matching vocabulary relaton normalises with — then inherit the base's `return self`, i.e. they answer with the **wrapper**. That is a *silently* wrong answer where the pre-reparenting `NoMethodError` was at least loud, so all five define both: `WrapperIdentifier`/`CompositeIdentifier`/`Bundled` peel through `base`, `Combined` through `identifiers.first` (the `ConsolidatedIdentifier` shape). **Known gap, pre-existing and deliberately left open:** CSA defines no URN shape for a container — `UrnGenerator` reads `publisher_prefix`/`number` straight off the identifier, which a container does not carry, so `to_urn` raises `NoMethodError`. It raised the identical error from the identical place on `main` (verified: 600 of 829 ids, both before and after), so nothing regressed; closing it needs a per-type URN-shape decision, because a bare delegation to `base` would collide a Canadian adoption with the standard it adopts. Pinned by a `root_number_spec.rb` block so the gap stays visible. **`Cec#number` is deliberately NOT memoised**: `@number` is the very ivar lutaml uses for the inherited attribute, so caching there would discard a value assigned through the generated writer and go stale if `cec_part`/`no_number` were reassigned — and `number` is the index key.
|
|
14
|
+
**Verified**: `to_s` **byte-identical** for all 829 ids that parsed on `main` (replayed from a captured baseline input list, not from the regenerated fixtures) and `to_urn` identical for all 829; `root.number` non-empty and raise-free for every parsing id; `from_hash(to_hash)` round-trips for every one; MR 0 raises / 0 unsafe characters / 0 collisions. Locked by `spec/pubid/csa/root_number_spec.rb` (structural tripwire + per-type key table + whole-corpus sweep over `spec/fixtures/csa/**`, which is what actually exercises the fixtures while `fixtures_spec.rb`'s glob stays broken; only meaningful under the full `bundle exec rake`), plus new CSA container rows in `root_spec.rb`, `identifier_roundtrip_spec.rb` (which carried a **pending** case for exactly this, now un-pended), `uniform_identifier_handle_spec.rb` and `mr_string_spec.rb` — none of which could carry a CSA wrapper row before. **relaton note**: there is no `relaton-csa` gem and no `relaton-data-csa`, so nothing published needs migrating; that is precisely why the serialized shape was changed properly rather than patched. (hand-off: csa-root-number-and-wrapper-hierarchy.)
|
|
15
|
+
|
|
16
|
+
- **All six CSA containers rendered plain under `to_s(annotated: true)`,
|
|
17
|
+
for two different reasons.** The five wrappers — `Bundled`,
|
|
18
|
+
`CanadianAdopted`, `Combined`, `CsaAdopted`, `Package` — compose their
|
|
19
|
+
own string, so they needed `annotate_plain_render`; but adding it alone
|
|
20
|
+
fixed none of them, because a wrapper carries no identity of its own and
|
|
21
|
+
the annotator was reading only the wrapper's attributes. The wrap plus
|
|
22
|
+
`Annotator#emit_tokens` walking `base`/`identifiers` is what closes them.
|
|
23
|
+
|
|
24
|
+
**`Cec` defines no `to_s` at all** and already reached the choke point
|
|
25
|
+
inside `render`. It printed plain because `CSA C22.2 NO. 286:23` stores
|
|
26
|
+
the joined `"C22.2-286"` in `number`, which appears nowhere in the output,
|
|
27
|
+
while the halves it does print sit in `cec_part` and `no_number`. It
|
|
28
|
+
overrides `annotation_tokens` to name those two plus `publisher_prefix`.
|
|
29
|
+
|
|
30
|
+
**`CsaAdopted` is the one to be careful with**: its `to_s` regex-rewrites
|
|
31
|
+
the string it gets from `base.to_s` (`:2015`→`:15`, `/Amd 1`→`/A1`). That
|
|
32
|
+
is safe only because it calls `base.to_s` with no options, so the base is
|
|
33
|
+
plain, and because the annotation wrap goes on the **outside**. Move the
|
|
34
|
+
wrap inward and those patterns would start matching inside `<span
|
|
35
|
+
class="year">2015</span>`.
|
|
36
|
+
|
|
37
|
+
## `Csa::Identifier#exclude` resets the whole year-format cluster, not just `year`
|
|
38
|
+
|
|
39
|
+
Moved from the root `CLAUDE.md`'s `#exclude` bullet (`metanorma__pubid__exclude-recursion-depth`), where it had grown that bullet past the 100,000-byte session-load budget `spec/pubid/flavor_notes_spec.rb` enforces.
|
|
40
|
+
|
|
41
|
+
**CSA follows the same scalar-year pattern** (`Csa::Identifier#exclude`): its `year` is a plain `:string`, but unlike BIPM the year carries *format metadata* stored in sibling attributes (`year_format`, `year_prefix`, `original_year_4digit`, and `french` — the last set **only** from a `:F` year prefix, the French-edition form `CSA B149.1:F20`). A year-less parse leaves all of these nil/false, so the override must reset **every** year-derived attribute (not just `year`) after `super`, or `==` — and thus `matches?` — would still diverge (e.g. a bare `CSA B149.1` would fail to match the French edition `CSA B149.1:F20`). The `respond_to?(:year=)` guard skips the adoption wrappers (`CanadianAdopted`/`CsaAdopted`), which delegate year to a nested inner id and lack a `year` accessor — those are handled by the base recursion.
|
data/lib/pubid/csa/builder.rb
CHANGED
|
@@ -253,7 +253,7 @@ module Pubid
|
|
|
253
253
|
|
|
254
254
|
# Code
|
|
255
255
|
if parsed_hash[:code]
|
|
256
|
-
series.number =
|
|
256
|
+
series.number = parsed_hash[:code].to_s
|
|
257
257
|
end
|
|
258
258
|
|
|
259
259
|
# Year format and year
|
|
@@ -520,13 +520,13 @@ module Pubid
|
|
|
520
520
|
# Pattern: "C22.1-15" should become code="C22.1", year="2015"
|
|
521
521
|
if !data[:year] && code_value =~ /^(.+)-(\d{2})$/
|
|
522
522
|
# Split code and year
|
|
523
|
-
identifier.number =
|
|
523
|
+
identifier.number = $1
|
|
524
524
|
# Convert 2-digit year to 4-digit
|
|
525
525
|
year_2digit = $2
|
|
526
526
|
identifier.year = "20#{year_2digit}"
|
|
527
527
|
identifier.year_format = "dash"
|
|
528
528
|
else
|
|
529
|
-
identifier.number =
|
|
529
|
+
identifier.number = code_value
|
|
530
530
|
end
|
|
531
531
|
end
|
|
532
532
|
|
|
@@ -669,3 +669,5 @@ module Pubid
|
|
|
669
669
|
end
|
|
670
670
|
end
|
|
671
671
|
end
|
|
672
|
+
|
|
673
|
+
Pubid::Csa::Builder.prepend(Pubid::Builder::AllPartsWrap)
|
data/lib/pubid/csa/identifier.rb
CHANGED
|
@@ -218,7 +218,8 @@ module Pubid
|
|
|
218
218
|
# Detect CSA adoption of international standards
|
|
219
219
|
# Examples: CSA ISO/IEC TR 12785-3:15, CSA CISPR 16-1-1:18, CSA IEC 60601-1:08
|
|
220
220
|
# CSA CEI/IEC 61000-4-28-01 (bilingual)
|
|
221
|
-
|
|
221
|
+
# CSA IWA 18:17 (the catalogue also drops the ISO keyword)
|
|
222
|
+
if input.match?(/^CSA (ISO\/IEC|CEI\/IEC|CISPR|IEC|CEI|ISO|IWA)\s/)
|
|
222
223
|
# This is CSA adoption of international standard
|
|
223
224
|
# Extract the wrapped standard portion
|
|
224
225
|
wrapped_input = input.sub(/^CSA\s+/, "")
|
|
@@ -495,6 +496,17 @@ module Pubid
|
|
|
495
496
|
end
|
|
496
497
|
end
|
|
497
498
|
|
|
499
|
+
# The catalogue also prints the IWA designation without the ISO
|
|
500
|
+
# keyword ("CAN/CSA-IWA 18:17" beside "CAN/CSA-ISO IWA 18:17");
|
|
501
|
+
# the ISO parser wants the ISO/ prefix.
|
|
502
|
+
if input.match?(/^IWA\s/)
|
|
503
|
+
begin
|
|
504
|
+
return Pubid::Iso.parse(input.sub(/^IWA\s/, "ISO/IWA "))
|
|
505
|
+
rescue StandardError
|
|
506
|
+
return nil
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
|
|
498
510
|
# Try CISPR (uses IEC parser)
|
|
499
511
|
if input.match?(/^CISPR\s/)
|
|
500
512
|
begin
|
|
@@ -47,7 +47,7 @@ module Pubid
|
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
base_str = (needs_space ? "#{prefix} " : prefix) +
|
|
50
|
-
base.number.
|
|
50
|
+
base.number.to_s + # Normalized code (e.g. "C22.2-1")
|
|
51
51
|
":#{year_display}"
|
|
52
52
|
parts = [base_str]
|
|
53
53
|
else
|
|
@@ -60,7 +60,7 @@ module Pubid
|
|
|
60
60
|
bundled_with.each do |bundled|
|
|
61
61
|
# For Cec identifiers, use normalized code format
|
|
62
62
|
if bundled.is_a?(Cec)
|
|
63
|
-
bundled_part = bundled.number.
|
|
63
|
+
bundled_part = bundled.number.to_s # e.g. "C22.2-2"
|
|
64
64
|
if bundled.year
|
|
65
65
|
# Use dash if year_format is dash, otherwise colon
|
|
66
66
|
separator = bundled.year_format == "dash" ? "-" : ":"
|
data/lib/pubid/csa/renderer.rb
CHANGED
|
@@ -115,7 +115,7 @@ module Pubid
|
|
|
115
115
|
|
|
116
116
|
# Reaffirmation - preserve original format and determine spacing
|
|
117
117
|
if id.reaffirmation && !id.reaffirmation.to_s.empty?
|
|
118
|
-
result += render_reaffirmation(id)
|
|
118
|
+
result += render_reaffirmation(id, year_prints_4digit: id.original_year_4digit == true)
|
|
119
119
|
end
|
|
120
120
|
|
|
121
121
|
# Package (already has leading space from parser)
|
|
@@ -225,11 +225,13 @@ module Pubid
|
|
|
225
225
|
|
|
226
226
|
private
|
|
227
227
|
|
|
228
|
-
# Reaffirmation rendering helper - shared across Base, Cec, and Series
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
228
|
+
# Reaffirmation rendering helper - shared across Base, Cec, and Series.
|
|
229
|
+
# The spacing follows the PRINTED year, not the parsed spelling: a
|
|
230
|
+
# 4-digit printed year glues ("C108.1.2-M1981(R2013)"), a 2-digit
|
|
231
|
+
# printed year takes a space ("C22.2 NO. 125-M84 (R2004)") — even
|
|
232
|
+
# when the input spelled the year 4-digit, since the CEC render
|
|
233
|
+
# always displays 2 digits.
|
|
234
|
+
def render_reaffirmation(id, year_prints_4digit: false)
|
|
233
235
|
# Check if reaffirmation was originally 4-digit (original_reaffirmation_4digit flag)
|
|
234
236
|
reaffirmation_was_4digit = id.original_reaffirmation_4digit
|
|
235
237
|
|
|
@@ -245,13 +247,11 @@ module Pubid
|
|
|
245
247
|
id.reaffirmation.to_s
|
|
246
248
|
end
|
|
247
249
|
|
|
248
|
-
# Determine spacing based on
|
|
249
|
-
if
|
|
250
|
-
# Year was 2-digit, reaffirmation was 4-digit → add space
|
|
251
|
-
" (R#{reaffirmation_str})"
|
|
252
|
-
else
|
|
253
|
-
# Both 2-digit, both 4-digit, or other cases → no space
|
|
250
|
+
# Determine spacing based on the printed year
|
|
251
|
+
if year_prints_4digit || !reaffirmation_was_4digit
|
|
254
252
|
"(R#{reaffirmation_str})"
|
|
253
|
+
else
|
|
254
|
+
" (R#{reaffirmation_str})"
|
|
255
255
|
end
|
|
256
256
|
end
|
|
257
257
|
|
|
@@ -33,6 +33,23 @@ module Pubid
|
|
|
33
33
|
attribute :package, :string # Package portion (Code, Handbook, etc.)
|
|
34
34
|
attribute :publisher_prefix, :string # Original prefix: "CAN/CSA-", "CSA", "CAN3-"
|
|
35
35
|
|
|
36
|
+
# Format flags that describe a value, keyed by that value. When the
|
|
37
|
+
# reference omits the value, a subset match skips its flags too: a bare
|
|
38
|
+
# `CSA C22.2 NO. 125` has `original_year_4digit` at its default `false`,
|
|
39
|
+
# which would otherwise refuse `CSA C22.2 NO. 125-M1984`.
|
|
40
|
+
SUBSET_FORMAT_FLAGS = {
|
|
41
|
+
year: %i[year_format year_prefix original_year_4digit french],
|
|
42
|
+
reaffirmation: %i[original_reaffirmation_4digit],
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
def subset_attribute_match?(name, mine, theirs)
|
|
46
|
+
return true if SUBSET_FORMAT_FLAGS.any? do |value, flags|
|
|
47
|
+
public_send(value).nil? && flags.include?(name)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
super
|
|
51
|
+
end
|
|
52
|
+
|
|
36
53
|
# True when the reference printed no publisher at all ("C22.1-15"), so
|
|
37
54
|
# rendering must not supply the default "CSA".
|
|
38
55
|
#
|
|
@@ -86,7 +103,7 @@ module Pubid
|
|
|
86
103
|
end
|
|
87
104
|
|
|
88
105
|
def number_from_kv(model, value)
|
|
89
|
-
model.number =
|
|
106
|
+
model.number = value.to_s
|
|
90
107
|
end
|
|
91
108
|
|
|
92
109
|
# `no_number` is a plain :string here but a Components::Code on Cec, so
|
data/lib/pubid/doi/builder.rb
CHANGED
data/lib/pubid/easc/builder.rb
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# ECMA flavor notes
|
|
2
|
+
|
|
3
|
+
ECMA edition and volume in the printed identifier, and the index key.
|
|
4
|
+
|
|
5
|
+
These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/ecma/` or `spec/pubid/ecma/`. The root file keeps the cross-flavor contract that every flavor obeys.
|
|
6
|
+
|
|
7
|
+
- **ECMA edition and volume — the one flavor whose `to_s` deliberately renders MORE than the document number**: `Relaton::Index::Type#add_or_update` keys on a **bare** `id.to_s` and cannot pass render options, so the default rendering **is** the index key. ECMA's renderer used to say outright that "the edition is never rendered", and 740 of the 804 published `relaton-data-ecma` rows carry one — so all 804 rows collapsed onto **421** keys and **383 were dropped on every crawl, silently** (`ECMA-74` 22 rows to 1, `ECMA-262` 18 to 1). `edition` and `volume` are therefore rendered **by default**, with `to_s(with_edition: false, with_volume: false)` as the opt-out — the inverse of pubid's usual opt-in, and per-flavor divergence in exactly the way 3GPP (`with_publisher: false`) and W3C (`true`) already diverge. **`volume` is a new `attribute :volume, :string`** (relaton's `:vol:`), needed by exactly four rows: ECMA-269 ed3 vol1-4 share one docidentifier **and one title**, so the volume is the only thing separating those four index rows — it is an index discriminator, not something the document prints about itself. Both are plain `:string`s, so `#exclude`/`#matches?` already treat them as wildcards with no override (`ECMA-269` matches every edition under `ignore: %i[edition volume]`); ECMA's `partial_ref_spec.rb` entry moved from `omits: []` to `%i[edition volume]`.
|
|
8
|
+
**Grammar (`lib/pubid/ecma/parser.rb`)**: ` ed<N>` (dotted — `ed5.1` is real, ECMA-402) and ` vol<N>` attach as two independent `.maybe`s **after** the `tr | mem | standard` alternation, so a technical report and a memento carry them exactly as a standard does. The `standard` branch also gained a **space separator** (`ECMA 6`), which is a **NORMALIZING parse** — it renders back as `ECMA-6`, so it lives in `spec/pubid/ecma/edition_volume_spec.rb` and must **never** enter the byte-exact `pass` fixtures (the BIPM `CIPM/2005-06(REV)` precedent). **Two PEG facts are load-bearing and easy to lose.** (1) `standard` cannot shadow ` TR/`/` MEM/` because after the space it demands a **digit** — verified by inverting the alternation order, which still parses both correctly. Keeping `tr | mem` first is belt-and-braces; the **disjoint first character** is the real invariant. (2) Appending a sequence moved the alternation **out of tail position**, so Parslet no longer threads `consume_all` into it and **will not backtrack into an alternative that already succeeded** (`rule(:alt) { str("ab") | str("a") }; rule(:root) { alt >> str("bc") }` fails on `"abc"`). A future branch matching a proper prefix of another branch's input would now fail the whole parse where before it was rescued. `maybe` is `repeat(0,1)` and does not backtrack either, so a marker without its number (` ed`, ` ed5.`, ` vol`) fails atomically and then fails `consume_all` — which is what rejects `ECMA-6 ed` and pins the canonical suffix order (`ECMA-6 vol1 ed3` is rejected).
|
|
9
|
+
**Render-option threading — `:with_volume` rides the `:trademark` route, deliberately.** `Pubid::Identifier#render` `except`s a flag from the rendering context and `slice`s it to the renderer; `:with_edition` was already in both, and `:trademark` is the precedent for an **IEEE-only** flag carried the same way. So `:with_volume` was added to those two lines and **`build_rendering_context`'s signature was left alone** — widening it would touch `Pubid::Iso::SingleIdentifier`'s strict override. The payoff is that **ECMA's `to_s` needed no change at all** and the renderer owns the defaults: no second transient ivar beside the pre-existing `@with_publisher`, so rendering stays stateless and reentrant. Residual blast radius, accepted: passing `with_volume:` to a *non*-ECMA identifier now reaches that flavor's renderer instead of dying in the context builder — `Renderers::HumanReadable` absorbs unknown opts by design, while ISO's three strict renderers (`DirectivesRenderer`/`IwaRenderer`/`SupplementRenderer`) raise, which they did before too.
|
|
10
|
+
**All three identity surfaces, and the two defects found by measuring them.** Per the project rule, edition and volume reach `to_s`, `to_urn` **and** `to_mr_string`. **URN**: labelled segments after the existing `part-<n>` (`urn:ecma:269:ed-3:vol-2`), purely additive — `UrnParser` probes for a labelled prefix (a `shift_labelled` helper using `delete_prefix`, not `sub`), so a URN minted before the segments existed still reads back. The **dot of a decimal edition is KEPT** (`urn:ecma:402:ed-5.1`): `.` is `unreserved` in RFC 8141 and the URN has an inverse, so it must be lossless. **MR**: ECMA defined **no** `mr_*` hooks, so (a) the base `mr_edition` (`edition&.number`) **raised `NoMethodError` on all 740 edition-carrying rows** — the BIPM/ASTM/ASHRAE crash again — and (b) `mr_publisher` was nil (the publisher is the `PUBLISHER` constant, not the inherited attribute) and `mr_type` was nil (the Builder picks a class, it never sets a `typed_stage`), so **`ECMA-101` and `ECMA TR/101` both slugged to `"101"`** and one output filename overwrote the other. Four hooks fix it, sanitising **by charset** (`gsub(/[^a-z0-9]+/, "-")`, the BIPM `mr_slug` precedent) rather than by an escape list. **The MR is deliberately lossy where the URN is not** (`ed5-1` vs `ed-5.1`): `Renderers::MrString` joins **segments** with `.`, so a dot inside one breaks the documented structure, and nothing parses an ECMA MR back (`Parsers::MrString::FLAVOR_MAP` has no ECMA entry). Say so in the comment rather than leaving the asymmetry looking accidental.
|
|
11
|
+
**New shared `mr_volume` slot.** `Renderers::MrString#render_flat` had no volume segment, so it gained one plus a **literal-nil** `mr_volume` hook on `Pubid::Identifier` — literally nil, **not** `volume&.to_s`, because `Pubid::Nist::Identifiers::Base` declares its own `volume` attribute with different MR semantics and a generic reader would silently move NIST's slug. Inert by construction (no flavor defined `mr_volume`, there is no `method_missing` in `lib/pubid`, and NIST overrides `to_mr_string` wholesale), and verified rather than assumed: a `main` baseline replay of `to_mr_string` over all **98,217** identifiers in `spec/fixtures/*/identifiers/pass/*.txt` is **byte-identical**. `render_flat` was rewritten around a frozen `SEGMENTS` list of hook names while it was open, which also cleared a pre-existing ABC offence.
|
|
12
|
+
**The corpus is the gate.** `spec/fixtures/ecma/identifiers/pass/index_corpus.txt` is the complete published index (804 rows) rendered as `"<:id:>[ ed<:ed:>][ vol<:vol:>]"`, generated from `relaton-data-ecma/index-v1.yaml`; `spec/pubid/ecma/corpus_spec.rb` asserts over all of it that every row parses and renders byte-exactly, that `to_s`/`to_urn`/`to_mr_string` each give **804 distinct** keys, that every slug is filename-safe, that the URN and the hash both round-trip, that `from_hash(to_hash) == id` (stronger than hash equality — `#matches?` is built on `==`), and that `root.number` is non-empty. **ECMA has no `identifiers/full/` corpus**, so `rake "validation:classify[ecma]"` cannot run and cannot clobber these files — its fixtures are hand-maintained, unlike every generated flavor. Note ECMA's `fixtures_spec.rb` glob is **correct** (two `..`, lowercase dir) and is not one of the ten dead ones. **Two existing specs asserted the inverted contract** and had to flip (`serialization_spec.rb`'s "omits edition from the printed string"), so a green suite before the code change was the failure signal, not the success one. **relaton note**: this unblocks `relaton__relaton__ecma-consume-index-v2`; `relaton-data-ecma`'s `index-v1` is string-keyed and no ECMA pubid index is published yet, so nothing needs migrating — but the crawl must run after this lands, or the rows key on the collapsed form again. (hand-off: ecma-edition-and-volume.)
|
|
13
|
+
|
|
14
|
+
## Subset match: strict attributes
|
|
15
|
+
|
|
16
|
+
Read `docs/SUBSET_MATCH.md` first. `===` reads a nil part of the reference as
|
|
17
|
+
a wildcard, which is wrong for the attributes below: the flavor models a nil
|
|
18
|
+
value as "this document has none". They are declared with `subset_strict`, so
|
|
19
|
+
`===` compares them exactly and a stated collection is not a prefix. A caller
|
|
20
|
+
that does want every part of a document sets `all_parts` on the reference, or
|
|
21
|
+
keeps `#matches?(other, ignore:)`.
|
|
22
|
+
|
|
23
|
+
- **`part` is strict** (`lib/pubid/ecma/identifier.rb`). `ECMA-418` is the
|
|
24
|
+
whole document, not part 1 of it, so `ECMA-418 === ECMA-418-1 ed1` is
|
|
25
|
+
false. relaton measured 5 spurious part rows for that one reference
|
|
26
|
+
before the declaration existed. `edition` and `volume` stay wildcards —
|
|
27
|
+
an omitted edition really does mean "any edition", which is why
|
|
28
|
+
`partial_ref_spec.rb` lists them under `omits`.
|
data/lib/pubid/ecma/builder.rb
CHANGED
|
@@ -20,6 +20,13 @@ module Pubid
|
|
|
20
20
|
# is only present for standards that split into parts (e.g. ECMA-418-1).
|
|
21
21
|
attribute :number, :string
|
|
22
22
|
attribute :part, :string
|
|
23
|
+
|
|
24
|
+
# A nil `part` means the document has no part, not "any part":
|
|
25
|
+
# `ECMA-418` is not `ECMA-418-1`. relaton measured 5 spurious part rows
|
|
26
|
+
# for that one reference. A caller that wants every part of a document
|
|
27
|
+
# sets `all_parts` on the reference.
|
|
28
|
+
subset_strict :part
|
|
29
|
+
|
|
23
30
|
# Edition is relaton's `:ed:` (the index stores {:id, :ed, :vol} and the
|
|
24
31
|
# YAML has edition.content). It is part of the printed identifier: the
|
|
25
32
|
# relaton index keys on a bare `to_s`, so without it all 22 editions of
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# ETSI flavor notes
|
|
2
|
+
|
|
3
|
+
ETSI index key, part exclusion and MR slug.
|
|
4
|
+
|
|
5
|
+
These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/etsi/` or `spec/pubid/etsi/`. The root file keeps the cross-flavor contract that every flavor obeys.
|
|
6
|
+
|
|
7
|
+
## From the root note "ETSI and OIML index key (`root.number`): the `code` component became flat leaf columns"
|
|
8
|
+
|
|
9
|
+
**(3) `exclude(:part)` needed an override.** With `parts` a real collection attribute the base `#exclude` reaches it and nils it — but a part-less reference parses with `parts` defaulting to `[]`, so `nil != []` would break `==` and therefore `#matches?` against exactly the reference the exclusion exists to match. `Pubid::Etsi::Identifier#exclude` resets it to `[]` after `super` (the "reset the WHOLE cluster" rule from CSA's year and IEEE's year/month/day); this also covers supplements, since the base recurses into the nested `base` through `exclude_from_nested`. The old `exclude_from_nested` `Components::Code` special-case was deleted as dead. **(4) The ETSI MR slug was rewritten, and this is the loud part of the change.** ETSI defined no `mr_*` hooks, and the base ones read the inherited `number`/`part`/`typed_stage`/`edition` — all nil for ETSI — so **every** ETSI slug was `etsi.<date>`: **454 distinct slugs for 24,724 documents**, 794 of them sharing `etsi.2018-07`, and `to_slug` is what consumers use as an output **filename**. `EtsiStandard` now supplies `mr_number_with_part` (number + `parts`), `mr_type` (the ETSI type token) and `mr_edition` (the `version`) — all four fields ETSI's `==` compares, per the rule that an identity-bearing marker must reach **every** identity surface, not just `==`. `SupplementIdentifier` gained `mr_supplement_suffix` so the shared renderer **recurses into `base`** instead of slugging the supplement flat off its own ordinal (which made every `/C1` corrigendum of one month share `etsi.1.<date>`) — the ITU `AnnexOfRecommendation` precedent. A shared **`mr_sanitize`** filters **by charset** (`[^a-z0-9-]` → `-`), not by an enumerated escape list: an ETSI number legitimately contains a **space** (`300 175`, `GSM 02.01`), which is outside the `[a-z0-9.-]` charset `Renderers::MrString` documents, and a dot inside a segment would break that renderer's `.`-joined segment structure (the BIPM `bipm.si-brochure.9e-v3-01.e` precedent). Result: **24,724 distinct slugs for 24,724 ids, 0 collisions, 0 unsafe characters.** OIML's MR is **unchanged** — it already had its own `mr_number_with_part` reading through `code`, which the derived reader keeps working.
|
|
10
|
+
|
|
11
|
+
## Subset match: strict attributes
|
|
12
|
+
|
|
13
|
+
Read `docs/SUBSET_MATCH.md` first. `===` reads a nil part of the reference as
|
|
14
|
+
a wildcard, which is wrong for the attributes below: the flavor models a nil
|
|
15
|
+
value as "this document has none". They are declared with `subset_strict`, so
|
|
16
|
+
`===` compares them exactly and a stated collection is not a prefix. A caller
|
|
17
|
+
that does want every part of a document sets `all_parts` on the reference, or
|
|
18
|
+
keeps `#matches?(other, ignore:)`.
|
|
19
|
+
|
|
20
|
+
- **`parts` is strict**, on the leaf `EtsiStandard` and on
|
|
21
|
+
`Etsi::Components::Code`. A stated part list is not a prefix, so
|
|
22
|
+
`ETSI TS 129 198-4 === ETSI TS 129 198-4-5` is false. This was the
|
|
23
|
+
largest case relaton measured: over 10,595 bare type+code queries the
|
|
24
|
+
matched set changed for 158 and the `best_match` winner for 122, and
|
|
25
|
+
`ETSI TS 129 198-4` resolved to `-4-5`.
|
|
26
|
+
- The declaration sits **on the leaf**, beside the flat columns and for the
|
|
27
|
+
same reason — `EtsiStandard` owns `number`/`minor`/`parts`, while the
|
|
28
|
+
shared `Pubid::Etsi::Identifier` is also `SupplementIdentifier`'s parent.
|
|
29
|
+
`Etsi::Components::Code` carries the same declaration although `===`
|
|
30
|
+
cannot reach it today (`#code` is a derived reader, not an attribute), so
|
|
31
|
+
the two shapes cannot drift.
|
|
32
|
+
- `SUBSET_PAIRS["etsi"]` in `spec/pubid/subset_match_spec.rb` moved from
|
|
33
|
+
`ETSI EN 300 175` to `ETSI EN 300 175-1`: the reference now states its
|
|
34
|
+
part, and the version and the date are what it omits.
|
data/lib/pubid/etsi/builder.rb
CHANGED
|
@@ -14,10 +14,16 @@ module Pubid
|
|
|
14
14
|
# Stays independent of Pubid::Components::Code because ETSI uses
|
|
15
15
|
# +minor+ (a flavor-specific sub-number) plus +parts+.
|
|
16
16
|
class Code < Lutaml::Model::Serializable
|
|
17
|
+
include ::Pubid::SubsetMatch
|
|
18
|
+
|
|
17
19
|
attribute :number, :string # Main number
|
|
18
20
|
attribute :minor, :string # Optional minor part
|
|
19
21
|
attribute :parts, :string, collection: true, default: [] # Parts array
|
|
20
22
|
|
|
23
|
+
# Mirrors the flat `parts` column of EtsiStandard, which owns the
|
|
24
|
+
# identity; this component is derived from it by `#code`.
|
|
25
|
+
subset_strict :parts
|
|
26
|
+
|
|
21
27
|
# Render code with space for minor and dash-separated parts
|
|
22
28
|
def to_s
|
|
23
29
|
result = number.to_s
|
|
@@ -8,6 +8,8 @@ module Pubid
|
|
|
8
8
|
# Represents an ETSI version string
|
|
9
9
|
# Format: V1.2.3, V2.0.0, or ed.1
|
|
10
10
|
class Version < Lutaml::Model::Serializable
|
|
11
|
+
include ::Pubid::SubsetMatch
|
|
12
|
+
|
|
11
13
|
attribute :version, :string # e.g., "1.1.1", "2.0.0", "1"
|
|
12
14
|
attribute :is_edition, :boolean, default: -> {
|
|
13
15
|
false
|
|
@@ -44,6 +44,13 @@ module Pubid
|
|
|
44
44
|
attribute :minor, :string
|
|
45
45
|
attribute :parts, :string, collection: true, default: -> { [] }
|
|
46
46
|
|
|
47
|
+
# The reference always states its parts. An empty list means the
|
|
48
|
+
# document has no part, and a stated list is not a prefix:
|
|
49
|
+
# `ETSI TS 129 198-4` is not `ETSI TS 129 198-4-5`. Over 10,595 bare
|
|
50
|
+
# ETSI queries relaton measured 122 changed `best_match` winners
|
|
51
|
+
# without this. A caller that wants every part sets `all_parts`.
|
|
52
|
+
subset_strict :parts
|
|
53
|
+
|
|
47
54
|
# Compact serialization (mirrors ISO/JCGM/OIML): the code fields are
|
|
48
55
|
# bare scalars (`number`, a `parts` array and, rarely, a `minor`);
|
|
49
56
|
# Version flattens to a scalar `version` string with an `is_edition`
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# EVS flavor notes
|
|
2
|
+
|
|
3
|
+
- **The wrapped document is `base`, the uniform parent accessor**: EVS shipped
|
|
4
|
+
with `adopted_identifier`, the name the rest of the codebase already retired.
|
|
5
|
+
The rename covers the Ruby attribute **and** the serialization key, so an EVS
|
|
6
|
+
row now reads `{"_type" => "pubid:evs:national-adoption", "base" => {…}}`.
|
|
7
|
+
**The name is load-bearing, not cosmetic.** `Pubid::Identifier#root` walks
|
|
8
|
+
`base`, so every EVS identifier used to be its own root with a nil `number`,
|
|
9
|
+
and relaton keyed all of them under the empty string `""` — the index-key
|
|
10
|
+
defect this repository records for wrappers in many other flavors.
|
|
11
|
+
`root.number` is now the origin standard's number (`"9001"` for
|
|
12
|
+
`EVS-EN ISO 9001:2015/A1:2024`, through two wrapper layers, because `#root`
|
|
13
|
+
recurses). `Renderers::Annotator#emit_tokens` also walks `base`, so
|
|
14
|
+
`to_s(annotated: true)` now annotates the wrapped document instead of
|
|
15
|
+
returning a bare string.
|
|
16
|
+
**What moved and what did not**: `to_s`, `to_urn` and the identifier classes
|
|
17
|
+
are unchanged; `to_hash` changes one key name, and `to_s(annotated: true)`
|
|
18
|
+
gains spans. EVS has no published `relaton-data-evs` index, so nothing stored
|
|
19
|
+
needs a migration — but the **pubid-testsuite corpus rows carry the old key**
|
|
20
|
+
and report `canonical hash` for all 10 EVS cases until the flavor is
|
|
21
|
+
re-exported. `to_mr_string` is still `""` for EVS (pre-existing: EVS supplies
|
|
22
|
+
no `mr_*` hooks).
|
|
23
|
+
Locked by the "uniform parent accessor" block in `spec/pubid/evs/evs_spec.rb`.
|
|
24
|
+
|
|
25
|
+
- **`from_hash` raised on every EVS hash — a raw default, not load order
|
|
26
|
+
(pubid#383)**: `Pubid::Evs::Identifier.from_hash(id.to_hash)` raised
|
|
27
|
+
`Lutaml::Model::InvalidFormatError` (`undefined method 'key?' for an instance
|
|
28
|
+
of Symbol`). The cause was one line in
|
|
29
|
+
`lib/pubid/evs/identifiers/national_adoption.rb`:
|
|
30
|
+
`attribute :type, Components::Type, default: -> { self.class.type[:key] }`.
|
|
31
|
+
The default was the Symbol `:evs_en`, not a `Components::Type`. The canonical
|
|
32
|
+
`to_hash` drops a default-valued attribute, so no row carries `type`. On
|
|
33
|
+
`from_hash`, lutaml finds no `type` key, resolves the default, and casts it
|
|
34
|
+
into the component through `Components::Type.from_hash(:evs_en)`, which
|
|
35
|
+
raises. The nested cross-flavor wrapped identifier was never the problem:
|
|
36
|
+
it deserializes alone through `Pubid.from_hash`.
|
|
37
|
+
**The fix landed on `main` in `ea6cab30`**, which gives EVS and all six IDF
|
|
38
|
+
classes a `self.class.default_type` class method returning
|
|
39
|
+
`Components::Type.new(abbr: type[:short])` — the cen_cenelec shape. Nothing in
|
|
40
|
+
EVS reads `type` (builder, renderer, URN generator and URN parser), and `type`
|
|
41
|
+
is still absent from the hash, so nothing rendered or serialized moved.
|
|
42
|
+
**The issue thread called it process-shape dependent. It is not.** It raises
|
|
43
|
+
in every load order tried (plain `require`, `Pubid.eager_load_flavors!`, CEN
|
|
44
|
+
loaded first, BSI loaded first), and all 10 corpus cases raised on `main`,
|
|
45
|
+
not only the one row the testsuite ledger names. The exporter looked clean
|
|
46
|
+
because `Conformance::Generator#round_trips?` rescues `StandardError` and
|
|
47
|
+
returns `false`, so a raise there never reached the report.
|
|
48
|
+
**Do not read `self.class` inside the default lambda body.** lutaml calls the
|
|
49
|
+
lambda with `instance_exec` when it has an instance, but calls it plainly
|
|
50
|
+
(`options[:default].call`) when it has none, and `self.class` is then `Class`.
|
|
51
|
+
A `default_type` class method keeps that resolution in one place.
|
|
52
|
+
**Two specs guard the bug class, and they are complementary**:
|
|
53
|
+
`spec/pubid/evs/from_hash_spec.rb` greps `lib/` for the one raw spelling, so a
|
|
54
|
+
copy of that exact line fails fast; `spec/pubid/component_attribute_default_spec.rb`
|
|
55
|
+
**evaluates** every component-typed default of every identifier class of every
|
|
56
|
+
flavor, so a raw default of any spelling or component type fails, whatever
|
|
57
|
+
flavor introduces it. Its `PENDING_RAW_DEFAULTS` table is empty today; an
|
|
58
|
+
entry that starts to pass turns red, which is the signal to delete it.
|
data/lib/pubid/evs/builder.rb
CHANGED
|
@@ -17,10 +17,12 @@ module Pubid
|
|
|
17
17
|
adopted = Pubid::CenCenelec::Builder.build(data[:adopted])
|
|
18
18
|
|
|
19
19
|
Identifiers::NationalAdoption.new(
|
|
20
|
-
|
|
20
|
+
base: adopted,
|
|
21
21
|
separator: data[:evs_separator] || "-",
|
|
22
22
|
)
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
|
+
|
|
28
|
+
Pubid::Evs::Builder.prepend(Pubid::Builder::AllPartsWrap)
|
data/lib/pubid/evs/identifier.rb
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
module Pubid
|
|
4
4
|
module Evs
|
|
5
5
|
# An EVS national adoption wraps a CEN identifier the same way BSI's
|
|
6
|
-
# AdoptedEuropeanNorm does — polymorphic `
|
|
6
|
+
# AdoptedEuropeanNorm does — polymorphic `base` so the
|
|
7
7
|
# wrapped object stays a real Pubid::CenCenelec identifier.
|
|
8
8
|
#
|
|
9
9
|
# Examples:
|
|
10
|
-
# "EVS-EN 18216:2026" (
|
|
11
|
-
# "EVS-EN ISO 14001:2026" (
|
|
12
|
-
# "EVS-EN ISO/IEC 27017:2026" (
|
|
13
|
-
# "EVS-EN ISO 9001:2015/A1:2024" (
|
|
10
|
+
# "EVS-EN 18216:2026" (base: EN 18216:2026)
|
|
11
|
+
# "EVS-EN ISO 14001:2026" (base: EN ISO 14001:2026)
|
|
12
|
+
# "EVS-EN ISO/IEC 27017:2026" (base: EN ISO/IEC 27017:2026)
|
|
13
|
+
# "EVS-EN ISO 9001:2015/A1:2024" (base: EN ISO 9001:2015/A1:2024)
|
|
14
14
|
class Identifier < ::Pubid::Identifier
|
|
15
|
-
attribute :
|
|
15
|
+
attribute :base, ::Pubid::Identifier, polymorphic: true
|
|
16
16
|
# "-" or " " — preserves the printed separator ("EVS-EN" vs "EVS EN")
|
|
17
17
|
attribute :separator, :string, default: -> { "-" }
|
|
18
18
|
|
|
@@ -6,10 +6,15 @@ module Pubid
|
|
|
6
6
|
# The EVS national adoption of a European Standard.
|
|
7
7
|
# Example: "EVS-EN 18216:2026"
|
|
8
8
|
class NationalAdoption < ::Pubid::Evs::Identifier
|
|
9
|
-
attribute :type, Components::Type, default: -> { self.class.
|
|
9
|
+
attribute :type, Components::Type, default: -> { self.class.default_type }
|
|
10
|
+
|
|
11
|
+
def self.default_type
|
|
12
|
+
Components::Type.new(abbr: type[:short])
|
|
13
|
+
end
|
|
10
14
|
|
|
11
15
|
def self.type
|
|
12
|
-
{ key: :evs_en,
|
|
16
|
+
{ key: :evs_en, short: "EN",
|
|
17
|
+
title: "EVS National Adoption of a European Standard" }
|
|
13
18
|
end
|
|
14
19
|
end
|
|
15
20
|
end
|
data/lib/pubid/evs/renderer.rb
CHANGED
|
@@ -17,7 +17,7 @@ module Pubid
|
|
|
17
17
|
EVS_NAMESPACE = "urn:evs:"
|
|
18
18
|
|
|
19
19
|
def generate
|
|
20
|
-
adopted_urn = identifier.
|
|
20
|
+
adopted_urn = identifier.base.to_urn
|
|
21
21
|
unless adopted_urn.start_with?(CEN_NAMESPACE)
|
|
22
22
|
raise Errors::ParseError,
|
|
23
23
|
"expected adopted CEN URN, got #{adopted_urn.inspect}"
|