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
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
|
#
|
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
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# GB (Chinese Standard) flavor notes
|
|
2
|
+
|
|
3
|
+
`Pubid::Gb` covers national (`GB`), confidential national (`GBn`),
|
|
4
|
+
sector/industry (`JB`, `HB`, `NY`, …) and social-group (`T/{ORG}`) Chinese
|
|
5
|
+
standards. The mandate category follows a slash: `T` (recommended), `Z`
|
|
6
|
+
(guideline), or nothing (mandatory).
|
|
7
|
+
|
|
8
|
+
## A flavor `key_value` block REPLACES the base maps — list every attribute
|
|
9
|
+
|
|
10
|
+
`Pubid::Gb::Identifier` declared its own `key_value` block and mapped
|
|
11
|
+
`publisher_code`, `mandate`, `number`, `part` and `all_parts` — but not
|
|
12
|
+
`date`. lutaml does not merge such a block with the maps of
|
|
13
|
+
`::Pubid::Identifier`; it replaces them. So `to_hash` silently dropped the
|
|
14
|
+
publication year of **every** dated GB identifier:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
id = Pubid::Gb::Identifier.parse("GB/T 20223-2006")
|
|
18
|
+
id.to_s # => "GB/T 20223-2006"
|
|
19
|
+
id.year # => "2006"
|
|
20
|
+
id.to_hash # no year at all
|
|
21
|
+
Pubid::Gb::Identifier.from_hash(id.to_hash).to_s # => "GB/T 20223"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The year survived `to_s`, `exclude` and `matches?`, so nothing in the flavor's
|
|
25
|
+
own behaviour looked wrong. Only `from_hash(to_hash) == parse` saw it — the
|
|
26
|
+
silent failure mode this repository records elsewhere: `#matches?` is
|
|
27
|
+
`exclude(*ignore) == other.exclude(*ignore)`, so a parsed reference never
|
|
28
|
+
matched a `from_hash`-ed index row, and the lookup returned nothing with no
|
|
29
|
+
error. Two editions of one document (`GB/T 20223-2006` and `GB/T 20223-2014`)
|
|
30
|
+
also collapsed onto one hash.
|
|
31
|
+
|
|
32
|
+
The fix is one line — `map "date", to: :date` — because the shared flat-scalar
|
|
33
|
+
rules do the rest: `flatten_scalar_components` (in `Identifier#to_hash`) writes
|
|
34
|
+
a bare `"year" => "2006"` instead of a nested `date` component, and
|
|
35
|
+
`inflate_scalar_components` reads it back. GB declares no `year` attribute, so
|
|
36
|
+
the `date` → `year` rename applies.
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
Pubid::Gb::Identifier.parse("GB/T 20223-2006").to_hash
|
|
40
|
+
# => {"_type" => "pubid:gb:standard", "publisher_code" => "GB",
|
|
41
|
+
# "mandate" => "T", "number" => "20223", "year" => "2006"}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Lesson for any flavor with its own `key_value` block**: an attribute absent
|
|
45
|
+
from the block does not serialize. Assert `from_hash(id.to_hash) == id`, not
|
|
46
|
+
only `to_s` and `to_hash` — the two shapes that were already correct here.
|
|
47
|
+
|
|
48
|
+
GB publishes no `relaton-data-gb` index, so no re-crawl follows.
|
|
49
|
+
|
|
50
|
+
## `GBn` — a lowercase letter inside the publisher token
|
|
51
|
+
|
|
52
|
+
`GBn` is the confidential national series, and the only one of the 67 prefixes
|
|
53
|
+
the relaton GB flavor carries that did not parse. The publisher rule accepted
|
|
54
|
+
`[A-Z]{1,3}`, which consumed `GB` and then stopped at the `n`.
|
|
55
|
+
|
|
56
|
+
A PEG takes the first branch that matches and does not backtrack into the
|
|
57
|
+
alternatives, so the `str("GBn")` branch comes **before** the uppercase-only
|
|
58
|
+
branch in `rule(:publisher_code)`. `GBn`, `GBn/T` and `GBn/Z` now parse, and
|
|
59
|
+
`GBn GBn/T GBn/Z` are in `PREFIXES`, so `Pubid.parse` routes them.
|
|
60
|
+
|
|
61
|
+
All 67 relaton prefixes now round-trip a dated identifier.
|
|
62
|
+
|
|
63
|
+
## Decision: the em dash normalizes to an ASCII hyphen
|
|
64
|
+
|
|
65
|
+
Chinese portals print an em dash before the year (`T/ZS 0467—2023`). The parser
|
|
66
|
+
accepts both spellings and the renderer prints an ASCII hyphen:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
Pubid::Gb::Identifier.parse("T/ZS 0467—2023").to_s # => "T/ZS 0467-2023"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This is deliberate, not a defect. The two spellings give **equal** identifiers,
|
|
73
|
+
so a reference in either form matches the document. The alternative — a stored
|
|
74
|
+
separator attribute, the CIE `date_separator` shape — would make the two
|
|
75
|
+
spellings unequal and would need an `exclude` override. The relaton fixture
|
|
76
|
+
`spec/gb/fixtures/tgzaepi_001_2018.xml` records the em-dash form and is the one
|
|
77
|
+
place that must follow this decision.
|
|
78
|
+
|
|
79
|
+
## The series code lives in the inherited `publisher`
|
|
80
|
+
|
|
81
|
+
`GB`, `JB`, `GBn` and `T/GZAEPI` are stored in the `publisher` attribute
|
|
82
|
+
inherited from `::Pubid::Identifier`, a `Components::Publisher`. The flavor had
|
|
83
|
+
its own `publisher_code` string beside it while the inherited attribute stayed
|
|
84
|
+
nil, so two attributes described one value and the shared code that reads
|
|
85
|
+
`publisher` saw nothing.
|
|
86
|
+
|
|
87
|
+
The component serializes as a bare scalar, because the class adds itself to the
|
|
88
|
+
flat-scalar table — the CEN/CENELEC precedent, and for GB's own classes only:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
Pubid::Gb::Identifier.flat_scalar_components # => {..., publisher: "publisher"}
|
|
92
|
+
Pubid::Gb::Identifier.flat_scalar_fields # => {..., publisher: :body}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
{"_type" => "pubid:gb:standard", "publisher" => "GB",
|
|
97
|
+
"mandate" => "T", "number" => "20223", "year" => "2006"}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**The URN gained the series, which repairs a collision.** `GB 20223-2006` and
|
|
101
|
+
`GBn 20223-2006` are two documents and shared `urn:gb:20223:2006`; they now
|
|
102
|
+
give `urn:gb:gb:20223:2006` and `urn:gb:gbn:20223:2006`. The shared URN
|
|
103
|
+
generator lowercases the body on its own. The MR slug follows (`gb.20223.2006`,
|
|
104
|
+
`gbn.20223.2006`) and sanitizes the slash of a social-group code by itself
|
|
105
|
+
(`T/GZAEPI` → `t-gzaepi.001.2018`), so GB does not join the IEEE slash ledger.
|
|
106
|
+
Annotated rendering gained a `publisher` span for free.
|
|
107
|
+
|
|
108
|
+
**Note the parse-tree key keeps its name.** `rule(:publisher_code)` in the
|
|
109
|
+
parser still captures `:publisher_code`; parse-tree keys and attribute names
|
|
110
|
+
are different namespaces (the ASHRAE landmine). Only the attribute moved.
|
|
111
|
+
|
|
112
|
+
## Known gaps
|
|
113
|
+
|
|
114
|
+
`spec/pubid/gb/fixtures_spec.rb` now reads `spec/fixtures/gb/`, which nothing
|
|
115
|
+
read before (the `ten-dead-fixture-specs` class). It carries a tripwire example
|
|
116
|
+
asserting both globs are non-empty, because a wrong glob reports 0 examples
|
|
117
|
+
instead of a failure. GB has no `identifiers/full/identifiers.txt`, so the
|
|
118
|
+
fixtures are hand-written and `rake validation:classify[gb]` does not drive
|
|
119
|
+
them; the reader accepts both the plain and the generated line shapes.
|
|
120
|
+
|
|
121
|
+
The URN carries the series, the number and the year, but **not the mandate**:
|
|
122
|
+
`GB 20223-2006` and `GB/T 20223-2006` both give `urn:gb:gb:20223:2006`, and
|
|
123
|
+
their MR slugs are equal too. The mandatory and the recommended standard are
|
|
124
|
+
two documents, so this is a real collision, narrower than the one the series
|
|
125
|
+
repaired. There is also no `Pubid::Gb::UrnParser`, so a GB URN cannot be read
|
|
126
|
+
back. Nothing consumes a GB URN today, so both are recorded, not fixed.
|
|
127
|
+
|
|
128
|
+
## Not supported (deliberately)
|
|
129
|
+
|
|
130
|
+
These forms do not parse. Each is outside what the relaton GB flavor supports
|
|
131
|
+
today:
|
|
132
|
+
|
|
133
|
+
| form | what it is |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `DB11/T 123-2020`, `DB37 1234-2020` | local standards; a province code follows `DB` |
|
|
136
|
+
| `Q/SY 123-2020` | enterprise standard |
|
|
137
|
+
| `GB/T 1.1-2020/XG1-2021` | amendment sheet (修改单) |
|
|
138
|
+
|
|
139
|
+
`PREFIXES` lists bare `DB`, `DB/T` and `DB/Z`, so the province-numbered form
|
|
140
|
+
was intended but is not parsed.
|
data/lib/pubid/gb/builder.rb
CHANGED
|
@@ -17,14 +17,17 @@ module Pubid
|
|
|
17
17
|
def build(data)
|
|
18
18
|
code, mandate = split_mandate(data[:publisher_code].to_s)
|
|
19
19
|
|
|
20
|
-
Identifiers::Standard.new(
|
|
20
|
+
identifier = Identifiers::Standard.new(
|
|
21
21
|
publisher: ::Pubid::Components::Publisher.new(body: code),
|
|
22
22
|
mandate: mandate || data[:mandate]&.to_s,
|
|
23
23
|
number: data[:number].to_s,
|
|
24
24
|
part: data[:part]&.to_s,
|
|
25
25
|
date: date_for(data[:year]),
|
|
26
|
-
all_parts: !data[:all_parts].to_s.empty?,
|
|
27
26
|
)
|
|
27
|
+
|
|
28
|
+
# "(all parts)" names every part of the document, so it wraps the
|
|
29
|
+
# document, which holds no mark itself.
|
|
30
|
+
data[:all_parts].to_s.empty? ? identifier : identifier.to_all_parts
|
|
28
31
|
end
|
|
29
32
|
|
|
30
33
|
private
|
|
@@ -48,3 +51,5 @@ module Pubid
|
|
|
48
51
|
end
|
|
49
52
|
end
|
|
50
53
|
end
|
|
54
|
+
|
|
55
|
+
Pubid::Gb::Builder.prepend(Pubid::Builder::AllPartsWrap)
|
data/lib/pubid/gb/identifier.rb
CHANGED
|
@@ -7,6 +7,11 @@ module Pubid
|
|
|
7
7
|
# descend from this class, so a parsed GB id is always an instance of
|
|
8
8
|
# Pubid::Gb::Identifier.
|
|
9
9
|
class Identifier < ::Pubid::Identifier
|
|
10
|
+
# The all-parts identifier of this flavor.
|
|
11
|
+
def self.all_parts_class
|
|
12
|
+
Identifiers::AllParts
|
|
13
|
+
end
|
|
14
|
+
|
|
10
15
|
# The issuing body code as printed — "GB", "JB", "GBn", "T/GZAEPI" —
|
|
11
16
|
# lives in the `publisher` attribute inherited from ::Pubid::Identifier.
|
|
12
17
|
# The flat-scalar hooks below serialize it as a bare string, and the
|
|
@@ -25,8 +30,6 @@ module Pubid
|
|
|
25
30
|
# uses the dotted form (e.g. "5606.1" => number "5606", part "1").
|
|
26
31
|
attribute :part, :string
|
|
27
32
|
|
|
28
|
-
# All-parts flag — true for "GB/T 5606 (all parts)" forms.
|
|
29
|
-
attribute :all_parts, :boolean, default: -> { false }
|
|
30
33
|
|
|
31
34
|
# Polymorphic type map for lutaml key_value (de)serialization.
|
|
32
35
|
GB_TYPE_MAP = {
|
|
@@ -44,7 +47,6 @@ module Pubid
|
|
|
44
47
|
map "number", to: :number
|
|
45
48
|
map "part", to: :part
|
|
46
49
|
map "date", to: :date
|
|
47
|
-
map "all_parts", to: :all_parts, render_default: false
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
PUBLISHER = "CN"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Gb
|
|
5
|
+
module Identifiers
|
|
6
|
+
# Every part of one GB document: "GB/T 5606 (all parts)".
|
|
7
|
+
class AllParts < ::Pubid::Gb::Identifier
|
|
8
|
+
include ::Pubid::AllParts
|
|
9
|
+
|
|
10
|
+
# The document URN; GB and IDF mark no series in the URN.
|
|
11
|
+
def to_urn
|
|
12
|
+
identity.to_urn
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/pubid/gb/identifiers.rb
CHANGED
data/lib/pubid/gb/renderer.rb
CHANGED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# GOST flavor notes
|
|
2
|
+
|
|
3
|
+
Foreign-adoption routing: the slash-attached prefix boundary that let another
|
|
4
|
+
flavor win the routing race.
|
|
5
|
+
|
|
6
|
+
These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/gost/` or `spec/pubid/gost/`. The root file keeps the cross-flavor contract that every flavor obeys.
|
|
7
|
+
|
|
8
|
+
## Foreign-adoption routing must use a token-boundary prefix check, not a literal-space check
|
|
9
|
+
|
|
10
|
+
`Gost::Builder#parse_foreign` (`lib/pubid/gost/builder.rb`) resolves the
|
|
11
|
+
foreign standard a GOST document adopts (`"GOST 58904-2020/ISO/TR
|
|
12
|
+
25901-1:2016"` → the adopted `"ISO/TR 25901-1:2016"`) by routing through
|
|
13
|
+
`Pubid.prefix_flavors` first — the same "registered prefix, not load order"
|
|
14
|
+
mechanism the earlier `gost-adoption-prefix-routing` fix introduced for
|
|
15
|
+
`"GOST 1437-2024 (ASTM D129-18)"` — and falling back to an exhaustive,
|
|
16
|
+
alphabetically-sorted try-every-flavor loop only when no registered prefix
|
|
17
|
+
matches.
|
|
18
|
+
|
|
19
|
+
`prefix_owner`'s boundary check used to require a **literal space** after the
|
|
20
|
+
matched prefix (`raw.start_with?("#{p} ")`). That misses a form where the
|
|
21
|
+
type token is attached to the publisher with a **slash** instead of a space —
|
|
22
|
+
`"ISO/TR 25901-1:2016"`, `"ISO/TS 10303-1:2014"` — because the character
|
|
23
|
+
after `"ISO"` is `/`, not a space. Those strings therefore matched no
|
|
24
|
+
registered owner and fell through to the exhaustive fallback loop, racing
|
|
25
|
+
every registered flavor's grammar alphabetically.
|
|
26
|
+
|
|
27
|
+
**The race was real, not theoretical: `Pubid::Iec`'s own grammar also accepts
|
|
28
|
+
a bare `"ISO/TR 25901-1:2016"`** (as its own identifier, not as a delegation
|
|
29
|
+
to ISO), and renders it `"ISO TR 25901-1:2016"` — a space, IEC's own
|
|
30
|
+
convention, not ISO's. `Pubid::Bsi` also accepts the string and correctly
|
|
31
|
+
delegates to `Pubid::Iso::Identifiers::TechnicalReport`, and normally wins the
|
|
32
|
+
race because `"bsi"` sorts alphabetically before `"iec"` — which is exactly
|
|
33
|
+
why the wrong render (`GOST R 58904-2020/ISO TR 25901-1:2016` instead of
|
|
34
|
+
`.../ISO/TR 25901-1:2016`) was rare and reproduced only intermittently, and
|
|
35
|
+
only under full-suite load (hand-off
|
|
36
|
+
`metanorma__pubid__gost-idt-order-dependent-render.md`; the hand-off chased
|
|
37
|
+
GC/allocation timing and `RenderingContext` memoization as candidate causes —
|
|
38
|
+
neither was it, and neither needed to be pinned down, since routing this
|
|
39
|
+
deterministically by prefix removes the race regardless of what let `bsi`
|
|
40
|
+
occasionally lose it).
|
|
41
|
+
|
|
42
|
+
**Fix**: `prefix_owner` now uses the same token-boundary rule the top-level
|
|
43
|
+
router already uses (`Pubid.prefix_match?` in `lib/pubid.rb` — a non-word
|
|
44
|
+
character, not specifically a space, ends the prefix). That method used to be
|
|
45
|
+
`@api private`; it is now public for exactly this reason — a second,
|
|
46
|
+
hand-copied boundary check in `Gost::Builder` would be the same
|
|
47
|
+
duplicated-logic-drifts-out-of-sync hazard this file documents elsewhere for
|
|
48
|
+
other flavors (found in code review, not the first pass: the initial fix
|
|
49
|
+
duplicated the regex locally instead of calling the shared method). **Do
|
|
50
|
+
not** re-duplicate this boundary check in a flavor's own builder; call
|
|
51
|
+
`::Pubid.prefix_match?(string, prefix)`.
|
|
52
|
+
|
|
53
|
+
**Not affected**: a jointly-owned prefix (`"ISO/IEC"`, `"ISO/IEC TR"`) already
|
|
54
|
+
falls through to the exhaustive fallback by design (`owners.one?` is false),
|
|
55
|
+
and every owner renders it identically — verified for both the bare and the
|
|
56
|
+
type-attached compound forms. Only the single-owner, slash-attached-type
|
|
57
|
+
shape (`"ISO/TR"`, `"ISO/TS"`, and by the same rule `"IEC/TR"` etc.) was
|
|
58
|
+
broken.
|
|
59
|
+
|
|
60
|
+
Locked by `spec/pubid/gost/foreign_adoption_routing_spec.rb`, which includes
|
|
61
|
+
a deterministic reproduction of the race (not a reliance on catching the rare
|
|
62
|
+
full-suite failure): it temporarily `Registry.unregister(:bsi)`s the usual
|
|
63
|
+
race winner and asserts routing still resolves to `Pubid::Iso` correctly —
|
|
64
|
+
proving the fix is prefix routing, not a lucky alphabetical ordering.
|
data/lib/pubid/gost/builder.rb
CHANGED
|
@@ -166,7 +166,7 @@ module Pubid
|
|
|
166
166
|
def prefix_owner(raw)
|
|
167
167
|
index = ::Pubid.prefix_flavors
|
|
168
168
|
match = index.keys
|
|
169
|
-
.select { |p|
|
|
169
|
+
.select { |p| ::Pubid.prefix_match?(raw, p) }
|
|
170
170
|
.max_by(&:length)
|
|
171
171
|
return nil unless match
|
|
172
172
|
|
|
@@ -183,3 +183,5 @@ module Pubid
|
|
|
183
183
|
end
|
|
184
184
|
end
|
|
185
185
|
end
|
|
186
|
+
|
|
187
|
+
Pubid::Gost::Builder.prepend(Pubid::Builder::AllPartsWrap)
|
|
@@ -23,6 +23,11 @@ module Pubid
|
|
|
23
23
|
attribute :number, :string
|
|
24
24
|
attribute :year, :string
|
|
25
25
|
|
|
26
|
+
# A nil `copublisher` means GOST alone: `GOST R 27001` is not
|
|
27
|
+
# `GOST R ISO/IEC 27001`, which is an adoption of another body's
|
|
28
|
+
# document.
|
|
29
|
+
subset_strict :copublisher
|
|
30
|
+
|
|
26
31
|
GOST_TYPE_MAP = {
|
|
27
32
|
"pubid:gost:interstate-standard" => "Pubid::Gost::Identifiers::InterstateStandard",
|
|
28
33
|
"pubid:gost:national-standard" => "Pubid::Gost::Identifiers::NationalStandard",
|