pubid 2.0.0.pre.alpha.12 → 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/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/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/national_annex.rb +18 -20
- 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/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 +8 -0
- 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 +2 -0
- data/lib/pubid/csa/identifier.rb +13 -1
- data/lib/pubid/csa/renderer.rb +12 -12
- data/lib/pubid/csa/single_identifier.rb +17 -0
- 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 +2 -0
- data/lib/pubid/gb/CLAUDE.md +140 -0
- data/lib/pubid/gb/builder.rb +7 -2
- data/lib/pubid/gb/identifier.rb +5 -3
- data/lib/pubid/gb/identifiers/all_parts.rb +17 -0
- data/lib/pubid/gb/identifiers.rb +1 -0
- data/lib/pubid/gb/renderer.rb +0 -1
- 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 +161 -17
- data/lib/pubid/idf/builder.rb +6 -1
- data/lib/pubid/idf/identifier.rb +5 -0
- data/lib/pubid/idf/identifiers/all_parts.rb +17 -0
- data/lib/pubid/idf/identifiers.rb +1 -0
- 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 +134 -5
- 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/parser.rb +134 -21
- data/lib/pubid/ieee/renderer.rb +40 -7
- data/lib/pubid/ieee/urn_generator.rb +31 -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 +19 -5
- data/lib/pubid/iso/components/publisher.rb +2 -0
- data/lib/pubid/iso/identifier.rb +6 -14
- data/lib/pubid/iso/identifiers/all_parts.rb +19 -0
- data/lib/pubid/iso/identifiers/directives_supplement.rb +4 -2
- data/lib/pubid/iso/identifiers.rb +1 -0
- data/lib/pubid/iso/rendering_style.rb +0 -1
- data/lib/pubid/itu/CLAUDE.md +69 -0
- data/lib/pubid/itu/builder.rb +2 -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/supplement.rb +15 -0
- 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 +2 -0
- 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 +24 -3
- data/lib/pubid/nist/parser.rb +15 -2
- data/lib/pubid/nist/preprocessor.rb +53 -2
- 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 +20 -0
- 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/certification_system.rb +34 -0
- data/lib/pubid/oiml/identifiers/code_number.rb +8 -0
- data/lib/pubid/oiml/identifiers/dual_published.rb +174 -0
- data/lib/pubid/oiml/identifiers.rb +2 -0
- data/lib/pubid/oiml/parser.rb +35 -4
- data/lib/pubid/oiml/renderer.rb +23 -1
- data/lib/pubid/oiml/single_identifier.rb +4 -0
- data/lib/pubid/oiml/supplement_identifier.rb +7 -0
- data/lib/pubid/oiml/urn_generator.rb +28 -0
- 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/human_readable.rb +0 -1
- data/lib/pubid/sae/builder.rb +2 -0
- data/lib/pubid/sae/components/date.rb +2 -0
- data/lib/pubid/sae/components/type.rb +2 -0
- 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
- metadata +44 -2
|
@@ -31,6 +31,14 @@ module Pubid
|
|
|
31
31
|
attribute :suffix, :string
|
|
32
32
|
attribute :language, :string
|
|
33
33
|
|
|
34
|
+
# A nil `language` means the original English document, not any
|
|
35
|
+
# translation: `CCSDS 650.0-M-2` is not its French translation.
|
|
36
|
+
subset_strict :language
|
|
37
|
+
|
|
38
|
+
# A nil `suffix` means the current document, not its historical `-S`
|
|
39
|
+
# version: `CCSDS 101.0-B-4` is not `CCSDS 101.0-B-4-S`.
|
|
40
|
+
subset_strict :suffix
|
|
41
|
+
|
|
34
42
|
# Polymorphic type map for lutaml::Model key_value (de)serialization,
|
|
35
43
|
# mapping each concrete class's polymorphic_name to its class name.
|
|
36
44
|
CCSDS_TYPE_MAP = {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# CEN/CENELEC flavor notes
|
|
2
|
+
|
|
3
|
+
CEN/CENELEC supplements, adopted norms, equality and the relaton matching contract.
|
|
4
|
+
|
|
5
|
+
Read these notes before you change `lib/pubid/cen_cenelec/` or `spec/pubid/cen_cenelec/`. The root `CLAUDE.md` keeps the cross-flavor contract that every flavor obeys.
|
|
6
|
+
|
|
7
|
+
- **Six defects blocked the relaton CEN migration, and the parse was not one of them.** The relaton CEN flavor scrapes `standards.cencenelec.eu` and has no index, so it needs only `parse`, `exclude` and `matches?`. 63 of its 67 references rendered correctly, but the other surfaces failed: (1) `ENV 1613:1995` and `ES 59008:2000` rendered as `ENV/ENV …` and `ES/ES …`, because the renderer knew only CWA, HD and CR as publisher-types (BSI inherited it: `DD ENV 206:1992`, `PD ES 59008-1:2000`); (2) every consolidated identifier (`EN 285:2015+A1:2021`) raised on `exclude`, `to_hash` and `matches?`; (3) an adopted norm lost its CEN supplement (`CEN ISO/TS 21003-7:2008/A1:2010` → `CEN ISO/TS 21003-7:2008`, and 8 fixture lines such as `EN ISO 13485:2016/AC:2016`); (4) `to_hash` raised on every supplement and on every adopted norm; (5) **any two amendments were `==`**, so `matches?` on an amendment was always true; (6) `from_hash` raised on every CWA, HD, CR, ES and ENV document. The single constant `Pubid::CenCenelec::PUBLISHER_TYPES` (`CWA HD ES CR ENV`) now drives both the builder's class choice and the renderer. `spec/pubid/cen_cenelec/relaton_blockers_spec.rb` locks all six. (hand-off: `metanorma__pubid__cen-cenelec-relaton-blockers`.)
|
|
8
|
+
- **The supplements left the legacy `Identifiers::Base`.** `Amendment`, `Corrigendum`, `Fragment` and `ConsolidatedIdentifier` descended from `Identifiers::Base`, which declares a `publisher` String collection, `year`, `parts`, `type` and a hand-written `==` that compared only publisher, number, parts and year. A supplement sets none of those, so `EN 13250:2000/A1:2005 == EN 99999:1990/A7:1991` was true. They now descend from the shared `Pubid::CenCenelec::Identifier`, and `base`/`identifiers` are typed to that class (polymorphic), so a base can be a `SingleIdentifier`, an adopted norm or another supplement. The custom `==` is deleted; lutaml's attribute-wise `==` compares the whole object. `Identifiers::Base` itself stays, because it is Category B (`spec/pubid/identifier_hierarchy_spec.rb`), but no concrete type descends from it now. The top-level `SupplementIdentifier` (a subclass of it that no builder created) and its `UrnGenerator#generate_supplement_urn` branch were deleted: both still read the old `amendment_number`/`amendment_year` names. **The `:human` format registry moved to `Pubid::CenCenelec::Identifier`**: it was registered on `SingleIdentifier` and `Identifiers::Base` only, so a re-parented supplement rendered as `""`.
|
|
9
|
+
- **The deleted readers were the landmine, not a convenience.** `ConsolidatedIdentifier` defined `publisher`, `number`, `year`, `parts` and `type` readers that delegated to the first member, and `AdoptedEuropeanNorm` defined `number`, `year`, `date`, `parts` and `part` readers that delegated to the ISO/IEC document. Each name is a lutaml attribute, and `#exclude` and the serializer read every attribute through its reader: the consolidated one reached `EuropeanNorm#parts`, which does not exist, and the adopted one wrote the ISO values a second time at the top level of the hash. Read the document through `#base_document`, `#drop_supplements` or `#root` instead. The supplements expose `supplement_type`, and their ordinal and year as plain `number` / `year` — the same names BSI's supplements use (the `supplement_number` / `supplement_year` aliases were deleted; see the matching-primitives bullet in CLAUDE.md). **Consequence:** a supplement's own `publisher` is now nil; use `base_document.publisher`.
|
|
10
|
+
- **BSI read through the deleted readers.** `Bsi::Identifiers::AdoptedEuropeanNorm#number` (and `year`, `date`, `parts`, `part`) delegated to its adopted document, and for `BS EN ISO 11819-1:2023` that document is a CEN adoption, whose number came from the deleted reader. A private `delegate_target` now reads through the CEN adoption to the ISO standard. `subpart` keeps reading the CEN object, as before. A replay of all 1,501 BSI pass fixtures against `main` gave byte-identical `to_s`, `to_urn`, `to_mr_string`, class and `root.number`, except 5 repaired doubled publishers and 2 fragments (`BS EN IEC 61535/AMD1/FRAG1`) whose `root.number` changed from `"1"` to `"61535"`.
|
|
11
|
+
- **The year rule is BSI's, plus a CEN key (decided with the user).** `exclude(:year)` removes only the base document's year: `EN 13250:2000/A1:2005` → `EN 13250/A1:2005`, `EN 285:2015+A1:2021` → `EN 285+A1:2021`. The key `:supplement_year` removes the supplement's own date instead, and the month goes with the year, so a `/AC:2016-11` does not keep a month with no year. ISO and IEC behave differently: there each layer keeps its year in `date`, so `exclude(:year)` removes every year. The CEN rule lets relaton keep the base year strict and make the amendment year optional. **The mechanism used to be a per-flavor `#exclude` override; it is now the shared base class** (`metanorma__pubid__exclude-recursion-depth` hand-off, `lib/pubid/bsi/CLAUDE.md`'s "The `exclude` recursion boundary"). `::Pubid::Identifier#exclude` itself now protects the attributes a class names in `self.supplement_date_attributes` (`%i[year]` on `Amendment`, `%i[year month]` on `Corrigendum`, empty on a document — and empty on every flavor that doesn't override it, so this is a no-op everywhere else) from a bare `:date`/`:year`, or clears them instead when `:supplement_year` is passed. `Pubid::CenCenelec::Identifier#exclude` was deleted; the base #exclude still passes every key on to nested identifiers, so the members of a consolidated identifier follow the rule. **relaton ignore mapping**: a query with no part adds `%i[part subpart]`, a query with no base year adds `:year`, a query with no amendment year adds `:supplement_year`; `exclude(:year, :supplement_year)` gives the fully undated `EN 285+A1`.
|
|
12
|
+
- **The builder has one supplement path.** `Builder#build` builds the base document first (`build_base_document`: adopted norm, ENV adoption, implicit IEC adoption or plain identifier), then wraps it: a `/` supplement makes a standalone `Amendment`/`Corrigendum`, a `+` supplement makes a `ConsolidatedIdentifier`. The adopted branches used to return early, which is how they lost the supplement. The implicit IEC adoption (`EN 60xxx` → `EN IEC 60xxx`) now runs only when there is no supplement, because it rebuilds the identifier from the bare number and dropped a `+A1` the same way. An empty captured number or month is stored as nil (`present_string`), because `to_hash` drops `""` and `from_hash` then gives nil. A mixed `/` and `+` form keeps only the first `/` supplement; that gap is older than this change and is not fixed.
|
|
13
|
+
- **The adopted norm holds a `Components::Publisher`.** The builder passed `publisher: ["EN"]`, an Array, into a `Components::Publisher` attribute, and the renderer joined it; `to_hash` raised and the URN and MR printed the Array's `inspect`. The second publisher of a `CEN/CLC` adoption goes in `copublishers`, set only when present (an empty collection becomes nil after a round trip). `lib/pubid/bsi/builder.rb` constructs this class too and now takes the `EN` default. An ENV adoption (`ENV ISO 11079:1999`) now carries the publisher `ENV`, not the `EN` default.
|
|
14
|
+
- **A CEN supplement's year is a string.** `year` is `"2005"`, not `2005`, since it became a `:string` attribute (see the wire-shape bullet below). BSI's supplements were later aligned to the same type, so a caller comparing years across the two flavors compares like with like.
|
|
15
|
+
- **A publisher-type does not write its publisher (decided with the user).** `SingleIdentifier.default_publisher` is the type token for the five `PUBLISHER_TYPES` classes (`CenWorkshopAgreement` CWA, `HarmonizationDocument`/`CenelecHarmonizationDocument` HD, `CenReport` CR, `EuropeanSpecification` ES, `EuropeanPrestandard` ENV) and EN for every other class. `to_hash` leaves out a value equal to its default, so `CWA 14050-21:2000` is `{"_type" => "…cen-workshop-agreement", "number" => "14050", "part" => "21", "year" => "2000"}`: `_type` already names the publisher. An adopted norm is a European Norm, so `HD IEC 60364-8-81` and `CR ISO 13434:1999` still write `"publisher" => "HD"`/`"CR"`, and a TR, TS or Guide still writes `CEN` or `CLC`, which is real information. After this, every key left in a CEN hash carries information.
|
|
16
|
+
- **A draft stage serializes as one code (decided with the user).** `prEN 1234:2020` wrote three nested components, `type`, `stage` and `typed_stage`, each a copy of one entry of the stage registry (with its name and harmonized stages). It now writes `"stage" => "pren"` (`fpren` for FprEN), as ISO (`"dis"`) and IEC (`"cd"`) do. `Pubid::CenCenelec::Identifier.compact_hash` writes the short form only when the three components are exactly what the registry entry gives, so the round trip stays exact; `inflate_scalar_components` rebuilds them with `CenCenelec.locate_stage_by_code`, and the nested form still reads. `compact_hash` is a new class hook on `Pubid::Identifier` (no-op by default) that the canonicalizer calls for every identifier, nested ones included, so the stage of a `base` is short too. **The slug now names the stage**: `prEN 1234:2020` and `FprEN 1234:2020` both slugged `en.en.1234.2020` (the shared `mr_type` gives the type code `en`); a CEN `mr_type` override gives `en.pren.1234.2020` and `en.fpren.1234.2020`. **An adoption keeps its draft stage**: `prEN ISO 1234:2020` rendered as `EN ISO 1234:2020`, because `build_adopted_identifier` dropped the `type_with_stage` capture. It now stores the three stage components, the renderer prints `prEN` in place of the publisher, and the URN appends the stage as the base URN does (`urn:cen:en:iso:1234:2020:stage.proposal`).
|
|
17
|
+
- **The adopted document is `adopted`, in CEN and BSI together (decided with the user).** CEN `AdoptedEuropeanNorm`/`EuropeanPrestandard` (and the unused legacy `Identifiers::Base`) and BSI `AdoptedEuropeanNorm`/`AdoptedInternationalStandard` renamed `adopted_identifier` to `adopted`, the name GOST `IdenticalAdoption` already used; one rename across both flavors keeps a BSI-over-CEN chain on one name (`BS EN ISO 11819-1:2023` → `bsi.adopted.adopted`). The hash key follows (`"adopted" => {…}`). Nothing outside pubid read the old name and no published index stored it, so no alias is kept. The collections GOST `Harmonized#adopted_identifiers` and IEEE `AdoptedStandard#adopted_identifiers` keep their name: `relaton-data-ieee` stores 64 rows with that key.
|
|
18
|
+
- **The `type` default of a typed leaf is a component.** Nine leaves declared `default: -> { self.class.type[:key] }`, a bare Symbol such as `:cwa`, in a `Components::Type` attribute. `to_hash` dropped it as a default, and `from_hash` raised when it cast the Symbol. `SingleIdentifier.default_type` returns `Components::Type.new(abbr: type[:short])`, the same value the parser sets for `CEN/TR`. `to_hash` still omits it, and now also omits the `type` of TR, TS and Guide, which equals the default. No CEN index exists, so no stored row changes.
|
|
19
|
+
- **The wire shape is flat (decided with the user).** A supplement uses the same keys as a document: the inherited `number` holds the amendment, corrigendum or fragment number, and `year`/`month` are `:string` attributes (they were `amendment_number`, `amendment_year`, `corrigendum_*` and `fragment_number`, with Integer years). `EN 13250:2000/A1:2005` → `{"_type" => "…amendment", "base" => {…, "number" => "13250", "year" => "2000"}, "number" => "1", "year" => "2005"}`; `…/AC:2016-11` adds `"month" => "11"`. The `publisher` is a bare string and `copublishers` a list of strings (`"publisher" => "CEN", "copublishers" => ["CLC"]`). **This is CEN-only on purpose.** `Pubid::Identifier.flat_scalar_components`/`flat_scalar_fields` are now class methods over the shared tables, and `Pubid::CenCenelec::Identifier` merges `publisher`/`copublishers` into them; the shared canonicalizer gained collection support for this. A shared entry would also flatten ANSI, BSI, IDF and some IEEE hashes, and `relaton-data-ieee` is a published index. `from_hash` still reads the nested `{"body" => …}` form. A base document with a month still writes `"date" => {"year", "month"}`, because only a single-field component flattens. **A consolidated identifier writes its base once**: the members after the first carry no `base` (the builder used to copy it into each), so `EN 285:2015+A1:2021` is `{"identifiers" => [{EN 285:2015}, {"_type" => "…amendment", "number" => "1", "year" => "2021"}]}`. A member's `base_document` is therefore itself; walk the consolidated identifier instead.
|
|
20
|
+
- **URN and MR of the wrappers are new.** Every amendment had the URN `urn:cen:en`, and the 165 distinct fixture identifiers had only 130 distinct URNs. Now: `EN 13250:2000/A1:2005` → `urn:cen:en:13250:2000:amd:1:2005` / `en.13250.2000_amd.1.2005`; `EN 285:2015+A1:2021` → `urn:cen:en:285:2015:plus:amd:1:2021` / `en.285.2015_plus-amd.1.2021` (the `plus` marker, as in IEC, keeps it apart from the standalone `/A1:2021`); `EN 60038 AMD1 FRAG2` → `…:amd:1:frag:2`; an adoption uses its publisher and the adopted document's MR string (`CEN ISO/TS 21003-7:2019` → `urn:cen:cen:iso:ts:21003-7:2019` / `cen.iso.ts.21003-7.2019`). An unnumbered corrigendum has no number segment (`EN 13254:2000/AC:2003` → `…:cor:2003`). Over the fixture corpus, 187 distinct identifiers now give 187 distinct slugs and URNs.
|
|
21
|
+
- **The fixture spec was dead.** `spec/pubid/cen_cenelec/fixtures_spec.rb` globbed `../../../fixtures/…` and ran 0 examples. It now has the `spec/pubid/csa/fixtures_spec.rb` shape (tripwire, zero failures per file, conservation against `full/*.txt`). `full/cen.txt` now carries the relaton corpus. The three grammar gaps `prEN 13306 rev`, `prEN 285 rev` and `EN 285:2015/FprA1` are recorded in `fail/`; relaton skips them, because they are catalogue hits and not caller references. The regeneration also rewrote the `fail/` class column to `Pubid::Errors::ParseError`.
|
|
22
|
+
- **`Pubid::TypeResolver` could not resolve a CEN `_type`, which was older than the blocker fixes.** The polymorphic name is `pubid:cencenelec:…` (the module name), but the registry knows the flavor only as `cen_cenelec` and `cen`, so `Pubid::Registry.get("cencenelec")` was nil. `Pubid::CenCenelec::Identifier.from_hash` worked, because the flavor's own `polymorphic_type_map` resolves the type first, but `Pubid::Identifier.from_hash` on a CEN hash and a CEN identifier nested in another flavor's `::Pubid::Identifier` attribute (BSI `AdoptedEuropeanNorm#adopted`) deserialized as the abstract root. The flat publisher made this visible: the root cannot cast `"CEN"` into a `Components::Publisher`, so the wrong result became a raise. `TypeResolver#flavor_by_module_segment` now falls back to the registered module whose constant gives the segment; CEN is the only flavor with the mismatch (3GPP forces `pubid:3gpp:`). BSI round trips over its pass fixtures went from 815 to 865, and `KNOWN_ROUND_TRIP_FAILURES` for BSI from 647 to 597.
|
|
23
|
+
- **Still open, pinned nowhere as correct:** `HD IEC 60364-8-81 ED1` drops `ED1`; `EN 60038 AMD1 FRAG2` renders as `EN 60038/A1 FRAG2`; `root.number` of an adopted norm is the ISO `Components::Code` until ISO is retyped (tranche 3 of the `number` retype).
|
|
24
|
+
|
|
25
|
+
## CEN is the remaining `adopted` camp
|
|
26
|
+
|
|
27
|
+
BSI moved its adoption wrappers to the uniform `base` accessor (`lib/pubid/bsi/CLAUDE.md`); CEN did not, and this records the shape so the next branch does not have to re-survey it.
|
|
28
|
+
|
|
29
|
+
Three classes still name the parent slot `adopted`:
|
|
30
|
+
|
|
31
|
+
- `Identifiers::AdoptedEuropeanNorm` and `Identifiers::EuropeanPrestandard` — both carry a hand-written `#root` walking `adopted`, and an `mr_number_with_part` that puts `adopted.to_mr_string` where the number goes. So the index key and the slug are correct; only the accessor's name diverges, and `id.base` reads nil on both.
|
|
32
|
+
- `Identifiers::Base`, the legacy second root — its `adopted` is typed to CEN's own `Base`, **not** `::Pubid::Identifier`, so it cannot hold a cross-flavor adoption at all, and it has no `#root`. It is rarely instantiated (`SingleIdentifier` is the live branch), which is why nothing has tripped over the narrow type.
|
|
33
|
+
|
|
34
|
+
CEN is a harder rename than BSI was for one reason: `Pubid::Bsi::Builder` constructs a `CenCenelec::Identifiers::AdoptedEuropeanNorm.new(adopted: …)`, and `Bsi::Identifiers::AdoptedEuropeanNorm` used to read `target.adopted` through one CEN layer. The first of those two call sites is still there.
|
|
35
|
+
|
|
36
|
+
Note the asymmetry a rename must preserve: a BSI `AdoptedEuropeanNorm` now exposes `base`, and the CEN object it holds still exposes `adopted`, so a two-layer walk reads `id.base.adopted`. `#root` recurses through both regardless, which is why it — and not a delegation — is the right thing for a consumer to call.
|
|
37
|
+
|
|
38
|
+
## Subset match: strict attributes
|
|
39
|
+
|
|
40
|
+
Read `docs/SUBSET_MATCH.md` first. `===` reads a nil part of the reference as
|
|
41
|
+
a wildcard, which is wrong for the attributes below: the flavor models a nil
|
|
42
|
+
value as "this document has none". They are declared with `subset_strict`, so
|
|
43
|
+
`===` compares them exactly and a stated collection is not a prefix. A caller
|
|
44
|
+
that does want every part of a document sets `all_parts` on the reference, or
|
|
45
|
+
keeps `#matches?(other, ignore:)`.
|
|
46
|
+
|
|
47
|
+
- **`type`, `stage` and `typed_stage` are strict**
|
|
48
|
+
(`lib/pubid/cen_cenelec/identifier.rb`). A published European Norm holds
|
|
49
|
+
all three at nil, so a nil one means "published", not "any stage":
|
|
50
|
+
`EN 1325 === prEN 1325` and `EN 1991 === ENV 1991-2-2` are both false,
|
|
51
|
+
while `EN 1325 === EN 1325:2001` still holds. The three move together
|
|
52
|
+
because they repeat one entry of the stage registry — the same fact that
|
|
53
|
+
lets `compact_hash` write them as a single `"stage" => "pren"` and
|
|
54
|
+
`inflate_scalar_components` read them back, which is why the strict
|
|
55
|
+
comparison survives a `from_hash` round trip.
|
|
56
|
+
- The hand-off `subset-match-nil-means-none` called this a CEN-grammar
|
|
57
|
+
decision, on the reading that a published norm should carry a published
|
|
58
|
+
stage by default. It is fixed at the matching layer instead: no grammar
|
|
59
|
+
change, no index re-crawl.
|
|
@@ -95,10 +95,13 @@ module Pubid
|
|
|
95
95
|
base = locate_identifier_klass(base_data).new
|
|
96
96
|
assign_attributes(base, base_data)
|
|
97
97
|
|
|
98
|
-
# Build Amendment identifier wrapping the base
|
|
98
|
+
# Build Amendment identifier wrapping the base. The compact
|
|
99
|
+
# fragment spelling can carry the amendment's own year
|
|
100
|
+
# (EN 60038/A1:2009 FRAG2); the AMD-keyword spelling cannot.
|
|
99
101
|
amendment = Identifiers::Amendment.new(
|
|
100
102
|
base: base,
|
|
101
103
|
number: data[:amendment_number].to_s,
|
|
104
|
+
year: data[:amendment_year]&.to_s,
|
|
102
105
|
)
|
|
103
106
|
|
|
104
107
|
# Build Fragment wrapping the amendment
|
|
@@ -385,3 +388,5 @@ module Pubid
|
|
|
385
388
|
end
|
|
386
389
|
end
|
|
387
390
|
end
|
|
391
|
+
|
|
392
|
+
Pubid::CenCenelec::Builder.prepend(Pubid::Builder::AllPartsWrap)
|
|
@@ -20,6 +20,12 @@ module Pubid
|
|
|
20
20
|
attribute :part, :string
|
|
21
21
|
attribute :subpart, :string
|
|
22
22
|
|
|
23
|
+
# A published European Norm holds no type, stage or typed stage, so a
|
|
24
|
+
# nil one means "published", not "any stage": `EN 1325` is not
|
|
25
|
+
# `prEN 1325`, and `EN 1991` is not `ENV 1991`. The three components
|
|
26
|
+
# repeat one entry of the stage registry, so they move together.
|
|
27
|
+
subset_strict :type, :stage, :typed_stage
|
|
28
|
+
|
|
23
29
|
# The publisher serializes as a bare string ("publisher" => "CEN"), and
|
|
24
30
|
# the copublishers as a list of strings. The shared table does not do
|
|
25
31
|
# this for every flavor, because IEEE has a published index whose rows
|
|
@@ -81,33 +87,10 @@ module Pubid
|
|
|
81
87
|
stage.code.to_s.downcase
|
|
82
88
|
end
|
|
83
89
|
|
|
84
|
-
# The
|
|
85
|
-
#
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# The CEN year rule, as in BSI: `:year` removes only the base
|
|
91
|
-
# document's year ("EN 13250:2000/A1:2005" -> "EN 13250/A1:2005"), and
|
|
92
|
-
# the CEN key `:supplement_year` removes the supplement's own date
|
|
93
|
-
# ("EN 13250:2000/A1"). A supplement declares its date as a `year`
|
|
94
|
-
# attribute, which the base #exclude resets for `:year`, so the value is
|
|
95
|
-
# put back here. The month goes with the year, or a "/AC:2016-11" would
|
|
96
|
-
# keep a month with no year. The base #exclude passes every key on to
|
|
97
|
-
# the nested identifiers, so the members of a consolidated identifier
|
|
98
|
-
# follow the same rule.
|
|
99
|
-
def exclude(*args)
|
|
100
|
-
result = super
|
|
101
|
-
attrs = self.class.supplement_date_attributes
|
|
102
|
-
# `exclude(:amendment)` returns the base document, not a copy of self.
|
|
103
|
-
return result if attrs.empty? || !result.instance_of?(self.class)
|
|
104
|
-
|
|
105
|
-
drop = args.include?(:supplement_year)
|
|
106
|
-
attrs.each do |attr|
|
|
107
|
-
result.public_send(:"#{attr}=", drop ? nil : public_send(attr))
|
|
108
|
-
end
|
|
109
|
-
result
|
|
110
|
-
end
|
|
90
|
+
# The CEN year rule (`:year` vs `:supplement_year`) is the shared
|
|
91
|
+
# `::Pubid::Identifier#exclude` mechanism driven by `Amendment`'s and
|
|
92
|
+
# `Corrigendum`'s `supplement_date_attributes` — no override needed
|
|
93
|
+
# here. Detail: `lib/pubid/cen_cenelec/CLAUDE.md`'s year-rule bullet.
|
|
111
94
|
|
|
112
95
|
def self.parse(identifier)
|
|
113
96
|
unless identifier.is_a?(String)
|
|
@@ -36,20 +36,13 @@ module Pubid
|
|
|
36
36
|
base || self
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
#
|
|
40
|
-
#
|
|
39
|
+
# Names the supplement class, so callers need not special-case it. The
|
|
40
|
+
# ordinal and the year need no such method: Amendment and Corrigendum —
|
|
41
|
+
# in CEN and in BSI — all declare them as `number` and `year`.
|
|
41
42
|
def supplement_type
|
|
42
43
|
:amendment
|
|
43
44
|
end
|
|
44
45
|
|
|
45
|
-
def supplement_number
|
|
46
|
-
number
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def supplement_year
|
|
50
|
-
year
|
|
51
|
-
end
|
|
52
|
-
|
|
53
46
|
def mr_supplement_suffix
|
|
54
47
|
mr_join_segments("amd", number, year)
|
|
55
48
|
end
|
|
@@ -29,20 +29,13 @@ module Pubid
|
|
|
29
29
|
base || self
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
#
|
|
33
|
-
#
|
|
32
|
+
# Names the supplement class, so callers need not special-case it. The
|
|
33
|
+
# ordinal and the year need no such method: Amendment and Corrigendum —
|
|
34
|
+
# in CEN and in BSI — all declare them as `number` and `year`.
|
|
34
35
|
def supplement_type
|
|
35
36
|
:corrigendum
|
|
36
37
|
end
|
|
37
38
|
|
|
38
|
-
def supplement_number
|
|
39
|
-
number
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def supplement_year
|
|
43
|
-
year
|
|
44
|
-
end
|
|
45
|
-
|
|
46
39
|
# The date part of the supplement: "2016-11", "2003", or nil.
|
|
47
40
|
def supplement_date
|
|
48
41
|
return nil unless year
|
|
@@ -91,12 +91,20 @@ module Pubid
|
|
|
91
91
|
# Edition (ED2, ED3, etc.)
|
|
92
92
|
rule(:edition) { space >> str("ED") >> digits.as(:edition) }
|
|
93
93
|
|
|
94
|
-
# Fragment identifier (EN 60038 AMD1 FRAG2)
|
|
94
|
+
# Fragment identifier (EN 60038 AMD1 FRAG2, EN 60038/A1 FRAG2)
|
|
95
95
|
rule(:fragment_identifier) do
|
|
96
|
-
(stage_prefix | publisher) >>
|
|
96
|
+
((stage_prefix | publisher) >>
|
|
97
97
|
space >> number >> parts >>
|
|
98
98
|
space >> str("AMD") >> digits.as(:amendment_number) >>
|
|
99
|
-
space >> str("FRAG") >> digits.as(:fragment_number)
|
|
99
|
+
space >> str("FRAG") >> digits.as(:fragment_number)) |
|
|
100
|
+
# Compact supplement notation: the base may carry a year, the
|
|
101
|
+
# amendment uses the /A1 or +A1 join (with an optional year of
|
|
102
|
+
# its own) instead of the spelled-out "AMD1" keyword.
|
|
103
|
+
((stage_prefix | publisher) >>
|
|
104
|
+
space >> number >> parts >> year.maybe >>
|
|
105
|
+
(plus | slash) >> str("A") >> digits.as(:amendment_number) >>
|
|
106
|
+
(colon >> digit.repeat(4, 4).as(:amendment_year)).maybe >>
|
|
107
|
+
space >> str("FRAG") >> digits.as(:fragment_number))
|
|
100
108
|
end
|
|
101
109
|
|
|
102
110
|
# Adopted standard as opaque string - must start with org name
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# CIE flavor notes
|
|
2
|
+
|
|
3
|
+
CIE builder attribute assembly.
|
|
4
|
+
|
|
5
|
+
Read this before you change `lib/pubid/cie/` or `spec/pubid/cie/`. The root
|
|
6
|
+
`CLAUDE.md` keeps the cross-flavor contract that every flavor obeys.
|
|
7
|
+
|
|
8
|
+
- **The builder offered every CIE type the same attribute hash, and the types
|
|
9
|
+
do not share one attribute set.** `Builder#extract_attributes` unconditionally
|
|
10
|
+
set `attributes[:s_prefix]`, but only `Standard`, `DualPublished` and
|
|
11
|
+
`Identical` declare that attribute — `Conference`, `Bundle`, `Proceedings`,
|
|
12
|
+
`Supplement` and `TutorialBundle` do not. lutaml reads only declared
|
|
13
|
+
attribute names out of a constructor hash and reports nothing about the rest,
|
|
14
|
+
so the surplus key was discarded in silence and nothing failed.
|
|
15
|
+
|
|
16
|
+
It stopped being silent when `::Pubid::Identifier` began **refusing** an
|
|
17
|
+
unknown constructor key (the cross-flavor contract in the root `CLAUDE.md`):
|
|
18
|
+
**94 CIE identifiers immediately stopped parsing** with
|
|
19
|
+
`ArgumentError: unknown attribute for Pubid::Cie::Identifiers::Conference:
|
|
20
|
+
s_prefix` — every `CIE x005-1992`-style conference paper and every
|
|
21
|
+
comma-list bundle. The fix is one line in `lib/pubid/cie/builder.rb`:
|
|
22
|
+
select the assembled attributes down to those the chosen
|
|
23
|
+
`identifier_class` actually declares, immediately before `new`.
|
|
24
|
+
|
|
25
|
+
**The lesson is about where to filter, not about `s_prefix`.** A builder that
|
|
26
|
+
assembles one hash for a family of types must narrow it per type; the
|
|
27
|
+
alternative — letting the constructor drop what it does not recognise — is
|
|
28
|
+
exactly the silence that hid this. `Pubid::Builder::Base#assign_attributes`
|
|
29
|
+
already documents "silently skips unknown attributes" for the same reason,
|
|
30
|
+
but CIE's builder constructs directly and so bypassed it.
|
|
31
|
+
|
|
32
|
+
Locked by `spec/pubid/attribute_construction_spec.rb` ("builders offer only
|
|
33
|
+
declared attributes"), which parses a conference paper, a bundle and an
|
|
34
|
+
`s_prefix`-bearing standard, and asserts `Conference` still does **not**
|
|
35
|
+
declare `s_prefix` — so re-adding the attribute to silence the spec would
|
|
36
|
+
turn it red rather than green.
|
|
37
|
+
|
|
38
|
+
- **All 10 CIE types rendered plain under `to_s(annotated: true)`.** Every
|
|
39
|
+
one composes its own string instead of going through `render`, so none
|
|
40
|
+
reached the shared annotation hook; each now calls
|
|
41
|
+
`annotate_plain_render` on the way out.
|
|
42
|
+
|
|
43
|
+
**`Standard` needed more than a wrap.** Its body has three exits — the
|
|
44
|
+
`slash_colon` language form and the legacy bare slash-year form both
|
|
45
|
+
`return` early — and `annotate_plain_render` needs one. The composition
|
|
46
|
+
moved verbatim into a private `render_plain`; `to_s` is two lines. The
|
|
47
|
+
trap is the `private` keyword: written as a **section** it privatises
|
|
48
|
+
every method below it, which swallowed `mr_type` — a public method the
|
|
49
|
+
MR renderer calls. The section goes at the **end** of the class.
|
|
50
|
+
`Bundle` keeps its `return "" unless ids&.any?` guard and wraps only the
|
|
51
|
+
composed value.
|
|
52
|
+
|
|
53
|
+
**`Supplement` was a different bug with the same symptom.** It is a
|
|
54
|
+
wrapper: `number` is the supplement ordinal (`"1"`, which does not stand
|
|
55
|
+
alone in `CIE 121-SP1:2009`) and the document's identity — number `121`,
|
|
56
|
+
year `2009` — lives on `base`. Wrapping its `to_s` changed nothing,
|
|
57
|
+
because the annotator only read the wrapper's own attributes. It is fixed
|
|
58
|
+
by `Annotator#emit_tokens` walking `base`, not by anything in CIE.
|
data/lib/pubid/cie/builder.rb
CHANGED
|
@@ -12,6 +12,8 @@ module Pubid
|
|
|
12
12
|
# 3. Parenthetical: (DE), (ES), (en)
|
|
13
13
|
# 4. Translation year: (RU-2021)
|
|
14
14
|
class Language < Lutaml::Model::Serializable
|
|
15
|
+
include ::Pubid::SubsetMatch
|
|
16
|
+
|
|
15
17
|
attribute :code, :string # "E", "DE", "RU", "en"
|
|
16
18
|
attribute :format, :string # "slash", "slash_colon", "paren", "paren_year"
|
|
17
19
|
attribute :translation_year, :string # "2021" in "(RU-2021)"
|
|
@@ -28,6 +28,8 @@ module Pubid
|
|
|
28
28
|
# the prefix and any suffix notation (translation, reaffirmation,
|
|
29
29
|
# commentary) by calling adoption.render(context:).
|
|
30
30
|
class Adoption < Lutaml::Model::Serializable
|
|
31
|
+
include ::Pubid::SubsetMatch
|
|
32
|
+
|
|
31
33
|
attribute :base, ::Pubid::Identifier, polymorphic: true
|
|
32
34
|
attribute :adopter_publisher, ::Pubid::Components::Publisher
|
|
33
35
|
attribute :edition, :string
|
|
@@ -10,6 +10,8 @@ module Pubid
|
|
|
10
10
|
# composition rules — flavor-specific separators and orderings live in
|
|
11
11
|
# the subclass #to_s / #render override.
|
|
12
12
|
class Code < Lutaml::Model::Serializable
|
|
13
|
+
include ::Pubid::SubsetMatch
|
|
14
|
+
|
|
13
15
|
attribute :value, :string
|
|
14
16
|
attribute :prefix, :string
|
|
15
17
|
attribute :part, :string
|
|
@@ -12,6 +12,8 @@ module Pubid
|
|
|
12
12
|
# URN spec); flavors that drop the slot when undated gate that decision
|
|
13
13
|
# at the URN-generator level, not here.
|
|
14
14
|
class Date < Lutaml::Model::Serializable
|
|
15
|
+
include ::Pubid::SubsetMatch
|
|
16
|
+
|
|
15
17
|
attribute :year, :string
|
|
16
18
|
attribute :month, :string
|
|
17
19
|
attribute :day, :string
|
|
@@ -51,12 +53,6 @@ module Pubid
|
|
|
51
53
|
result
|
|
52
54
|
end
|
|
53
55
|
|
|
54
|
-
private
|
|
55
|
-
|
|
56
|
-
def pad2(value)
|
|
57
|
-
value.to_s.rjust(2, "0")
|
|
58
|
-
end
|
|
59
|
-
|
|
60
56
|
def hash
|
|
61
57
|
@hash ||= [year, month, day, undated].compact.map(&:hash).hash
|
|
62
58
|
end
|
|
@@ -67,6 +63,12 @@ module Pubid
|
|
|
67
63
|
year == other.year && month == other.month && day == other.day &&
|
|
68
64
|
undated == other.undated
|
|
69
65
|
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def pad2(value)
|
|
70
|
+
value.to_s.rjust(2, "0")
|
|
71
|
+
end
|
|
70
72
|
end
|
|
71
73
|
end
|
|
72
74
|
end
|
|
@@ -7,6 +7,8 @@ module Pubid
|
|
|
7
7
|
# Human render: "ED#{number}".
|
|
8
8
|
# URN render: "ed-#{number}" per RFC 5141-bis.
|
|
9
9
|
class Edition < Lutaml::Model::Serializable
|
|
10
|
+
include ::Pubid::SubsetMatch
|
|
11
|
+
|
|
10
12
|
attribute :year, :string
|
|
11
13
|
attribute :number, Lutaml::Model::Type::Value
|
|
12
14
|
attribute :original_text, :string
|
|
@@ -21,6 +21,8 @@ module Pubid
|
|
|
21
21
|
# - approved_amendments_flag: when true and no intermediate_amendments
|
|
22
22
|
# are listed, renders "... and its approved amendments".
|
|
23
23
|
class Relationship < Lutaml::Model::Serializable
|
|
24
|
+
include ::Pubid::SubsetMatch
|
|
25
|
+
|
|
24
26
|
REVISION_OF = "revision_of"
|
|
25
27
|
AMENDMENT_TO = "amendment_to"
|
|
26
28
|
CORRIGENDUM_TO = "corrigendum_to"
|
|
@@ -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
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
|