pubid 2.0.4 → 2.0.6
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/data/bipm/update_codes.yaml +14 -0
- data/data/parg/bipm.json +1 -1
- data/data/parg/bipm.parg +290 -0
- data/data/parg/iho.json +1 -1
- data/data/parg/iho.parg +358 -0
- data/data/parg/jis.json +1 -1
- data/data/parg/parg.json +1 -1
- data/data/parg/parg.parg +118 -0
- data/lib/pubid/bipm/CLAUDE.md +4 -0
- data/lib/pubid/bipm/builder.rb +26 -1
- data/lib/pubid/bipm/identifiers/committee_document.rb +13 -1
- data/lib/pubid/bipm/renderer.rb +18 -3
- data/lib/pubid/bipm/urn_generator.rb +14 -1
- data/lib/pubid/bipm/urn_parser.rb +14 -1
- data/lib/pubid/bipm.rb +10 -4
- data/lib/pubid/bsi/CLAUDE.md +4 -0
- data/lib/pubid/bsi/single_identifier.rb +10 -1
- data/lib/pubid/ieee/builder.rb +6 -1
- data/lib/pubid/ietf/CLAUDE.md +2 -0
- data/lib/pubid/ietf.rb +5 -2
- data/lib/pubid/iho/CLAUDE.md +157 -81
- data/lib/pubid/iho/builder.rb +61 -16
- data/lib/pubid/iho/identifier.rb +2 -2
- data/lib/pubid/iho/identifiers/base.rb +134 -37
- data/lib/pubid/iho/renderer.rb +44 -22
- data/lib/pubid/iho/urn_generator.rb +56 -19
- data/lib/pubid/iho/urn_parser.rb +211 -40
- data/lib/pubid/iso/CLAUDE.md +32 -0
- data/lib/pubid/iso/urn_parser.rb +63 -6
- data/lib/pubid/jis/CLAUDE.md +9 -4
- data/lib/pubid/parg/backend.rb +1 -1
- data/lib/pubid/parg/model.rb +7 -2
- data/lib/pubid/parg.rb +13 -0
- data/lib/pubid/tgpp/CLAUDE.md +2 -0
- data/lib/pubid/tgpp.rb +11 -0
- data/lib/pubid/version.rb +1 -1
- metadata +5 -2
data/lib/pubid/bipm.rb
CHANGED
|
@@ -6,13 +6,19 @@ module Pubid
|
|
|
6
6
|
extend Pubid::PrefixesSupport
|
|
7
7
|
|
|
8
8
|
# Leading tokens a printed BIPM identifier can start with: the 13 committee
|
|
9
|
-
# acronyms, "Metrologia" (journal),
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
9
|
+
# acronyms, "Metrologia" (journal), "BIPM" (the publisher prefix the SI
|
|
10
|
+
# Brochure / MEP / guide families print), and the two SI-family series
|
|
11
|
+
# tokens that can lead a reference on their own — "SI Brochure 9e …" and
|
|
12
|
+
# "SI MEP 10" are printed without the leading BIPM by consumers that strip
|
|
13
|
+
# it (relaton's Bibliography#search among them), so both spellings must
|
|
14
|
+
# route. JCGM is deliberately excluded — it is owned by the separate
|
|
15
|
+
# Pubid::Jcgm flavor, so BIPM never emits pubid:jcgm:* rows. French
|
|
16
|
+
# full-name forms that lead with a type word ("Recommandation 1 du CCL …")
|
|
17
|
+
# and the generic French word "Rapport" are omitted per the
|
|
13
18
|
# "exclude ambiguous tokens" policy.
|
|
14
19
|
PREFIXES = %w[
|
|
15
20
|
BIPM Metrologia
|
|
21
|
+
SI\ Brochure SI\ MEP
|
|
16
22
|
CIPM CGPM JCRB CCTF CCQM CCT CCL CCAUV CCU CCM CCEM CCPR CCRI
|
|
17
23
|
].freeze
|
|
18
24
|
|
data/lib/pubid/bsi/CLAUDE.md
CHANGED
|
@@ -91,3 +91,7 @@ The general fix (`metanorma__pubid__exclude-recursion-depth` hand-off) promotes
|
|
|
91
91
|
Verified: `spec/pubid/supplement_date_attributes_spec.rb` exercises the mechanism directly; `spec/pubid/bsi/parse_gaps_spec.rb`, `match_primitives_spec.rb` and `adoption_base_spec.rb`, and `spec/pubid/cen_cenelec/relaton_blockers_spec.rb`'s 10 pinned `:supplement_year` cases, all pass unchanged. Because the new logic only fires when `supplement_date_attributes` is non-empty, every other flavor (including ISO/IEC, which deliberately keep a supplement's year in `date` with no protection — see `lib/pubid/cen_cenelec/CLAUDE.md`) is untouched.
|
|
92
92
|
|
|
93
93
|
**Open question, not resolved here: `#dated_version_of?`/`#draft_of?` on a bare BSI supplement.** Both compare `exclude(:date, :year[, …])`, which — like every other bare `:date`/`:year` exclusion — now protects a supplement's own date unless `:supplement_year` is also passed. Two standalone `BS 7273-4:2015+A1:2021`/`...+A1:2022` objects were `dated_version_of?` each other before this branch (BSI's old `year`-attribute dodge never protected against `exclude(:year)`) and are not after. **Whether that's the right answer is genuinely unclear and deliberately left open pending feedback** — one reading is that this just makes BSI consistent with CEN/CENELEC, which already behaved this way before this branch (its pre-existing per-flavor `#exclude` override had the same protection); the other is that these two generic relational predicates should be widened to accept a `:supplement_year`-aware exclusion set so they can still recognize two differently-dated editions of the *same supplement* as related. (The predicates switched from `matches?(other, ignore: …)` to the equivalent `exclude(…)` comparison so NIST's asymmetric `#matches?` override no longer breaks them; BSI behavior is unchanged.) Locked (as current behavior, not as a verdict) by `spec/pubid/supplement_date_attributes_spec.rb`.
|
|
94
|
+
|
|
95
|
+
## The `BSI `-prefixed spelling of the BS series parses (pubid#644)
|
|
96
|
+
|
|
97
|
+
`BSI BS 5950` used to be a hard parse failure: the strip in `Pubid::Bsi::Identifier.parse` (`strip_bsi_prefix`) only dropped the `BSI ` prefix before PAS/PD/DD/BIP (`BSI_PREFIXED_SERIES`), and `BS` was deliberately absent because **BSI never prints `BSI` in front of its own BS series**. That remains true of the **render** — `to_s(with_publisher: true)` still answers `BS 5950` for a British Standard, and the `BSI` prefix is re-added only for `BSI_PREFIXED_SERIES` — but **external references do cite the series as `BSI BS 5950`**, so the prefixed spelling is now accepted on **parse**: `STRIPPABLE_SERIES = BSI_PREFIXED_SERIES + ["BS"]` drives the strip, and the input normalizes to the canonical `BS 5950` (a normalizing parse — never a byte-exact fixture). Two lists, one line apart, because the parse and render halves of the same question legitimately differ: `BSI_PREFIXED_SERIES` answers "which series does BSI *print* its publisher in front of", `STRIPPABLE_SERIES` answers "which series does an *external* citation print it in front of". Locked by `spec/pubid/router_printed_forms_spec.rb` (routing + canonical render + stable reparse).
|
|
@@ -37,7 +37,16 @@ module Pubid
|
|
|
37
37
|
# itself and is never preceded by it.
|
|
38
38
|
BSI_PREFIXED_SERIES = %w[PAS PD DD BIP].freeze
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
# Series tokens whose `BSI `-prefixed spelling parses (the strip below
|
|
41
|
+
# drops the prefix before the baked grammar runs). That is the
|
|
42
|
+
# BSI_PREFIXED_SERIES plus `BS`: `BS` is never PRINTED with the prefix
|
|
43
|
+
# by BSI itself, but external references do cite the British Standard
|
|
44
|
+
# series as `BSI BS 5950` (pubid#644), so the prefixed spelling is
|
|
45
|
+
# accepted on parse and normalizes to the canonical `BS 5950`. Render
|
|
46
|
+
# never re-adds the prefix for `BS` — only BSI_PREFIXED_SERIES does.
|
|
47
|
+
STRIPPABLE_SERIES = (BSI_PREFIXED_SERIES + %w[BS]).freeze
|
|
48
|
+
|
|
49
|
+
BSI_PREFIX = /\ABSI\s+(?=(?:#{STRIPPABLE_SERIES.join('|')})\b)/
|
|
41
50
|
|
|
42
51
|
# `BSI PAS 2031:2019` is the external spelling of `PAS 2031:2019`. The
|
|
43
52
|
# prefix is not part of the identity, so it is dropped before the baked
|
data/lib/pubid/ieee/builder.rb
CHANGED
|
@@ -1812,7 +1812,12 @@ module Pubid
|
|
|
1812
1812
|
# Optional attributes (excluding part/subpart since they're in code)
|
|
1813
1813
|
extract_optional(parsed, attributes, :edition)
|
|
1814
1814
|
extract_optional(parsed, attributes, :revision)
|
|
1815
|
-
|
|
1815
|
+
# A printed separator before the revision is notation residue: once the
|
|
1816
|
+
# revision is repositioned to the canonical face (and every corpus
|
|
1817
|
+
# expectation for these variants renders it absent), carrying rev_sep
|
|
1818
|
+
# would make the variant hash differ from its canonical equivalent.
|
|
1819
|
+
# Keep it only for a bare "Rev" with no number.
|
|
1820
|
+
extract_optional(parsed, attributes, :rev_sep) unless attributes[:revision] =~ /\A\d/
|
|
1816
1821
|
attributes[:rev_bare] = true if parsed[:rev_bare]
|
|
1817
1822
|
|
|
1818
1823
|
# Month/day - always extract if present (but not if already extracted from year)
|
data/lib/pubid/ietf/CLAUDE.md
CHANGED
|
@@ -5,3 +5,5 @@ IETF readiness for the unified relaton index.
|
|
|
5
5
|
These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/ietf/` or `spec/pubid/ietf/`. The root file keeps the cross-flavor contract that every flavor obeys.
|
|
6
6
|
|
|
7
7
|
- **IETF readiness for the unified relaton index (`relaton-data-ietf`)**: relaton is collapsing RFCs, the RFC sub-series (BCP/STD/FYI) and Internet-Drafts into **one** index keyed on the structured pubid hash (`pubid_class: ::Pubid::Ietf::Identifier`). That index is **all-or-nothing** — `Relaton::Index::FileIO#deserialize_id` raises on the *first* unparseable row and `#load_index` then rejects the whole index — so one bad id out of 176,862 means no consumer resolves *any* IETF reference. Four changes, all verified against the published corpora (`relaton-data-{rfcs,rfcsubseries,ids}`, branch `v2`). **(1) `draft_rest` accepts `.` and `A-Z`** (`match['a-zA-Z0-9+_.\-']`): exactly 50 of the 166,740 published draft ids failed without them (21 dotted `draft-ietf-pilc-2.5g3g-12`, 29 uppercase `draft-chapin-clnp-ISO8473-00`, 0 both), and a character census confirms the class now covers the corpus **exactly** — the only chars outside `[a-z0-9-]` anywhere in it are `. _ +` and `A-Z`. Widening is safe because the top-level alternation is keyed on the leading token (`RFC`/`BCP|STD|FYI`/`draft-`), so a wider draft tail can never make another branch match; `Builder#split_draft_version` needed no change (it inspects only the last three characters). The set stays **closed** — a space, `/` or `&` in a slug is crawler junk and still fails (pinned by negative examples). **(2) Zero-padded ids parse, grammar-level**: `rfc-index.xml` writes sub-series membership as `<is-also><doc-id>STD0066</doc-id></is-also>` (369 padded doc-ids) and RFCs as `RFC0001`, and relaton#109 forbids relaton normalizing them itself. The space is `.maybe` on both the `rfc` and `subseries` rules and `Builder#unpad` strips the pad (`/\A0+(?=\d)/`, a look-ahead so an all-zero number keeps its last digit), so `STD0066`/`STD 0066`/`STD66` all render the canonical `STD 66`. `unpad` is applied unconditionally, so the **spaced** padded form is normalized too — `RFC 0001` previously parsed and rendered back verbatim, and now renders `RFC 1` with a correspondingly changed hash and URN. That is deliberate (one canonical spelling per document) and has **zero** effect on the published corpora, which contain no padded row. **This is a normalizing parse, so the padded spellings must NEVER go into the byte-exact `spec/fixtures/ietf/identifiers/pass/`** — they live in `spec/pubid/ietf/zero_padded_spec.rb`, the BIPM `CIPM/2005-06(REV)` precedent. **(3) The Internet-Draft slug moved from `name` into `number`, and `series` became derived**: relaton bsearches on `id.root.number.to_s`, so with the slug in `name` all 166,740 drafts keyed to `""` and the narrowing bought nothing (with the slug: 43,564 buckets, median 2, worst 101). `name` is **dropped entirely** (no alias); the slug — leading `draft-` included — is the key, deliberately shared by a draft's unversioned aggregator row and every one of its versions so a document and its variants cluster. `series` is likewise **not stored**: `_type: pubid:ietf:std` already encodes it, so `Bcp`/`Std`/`Fyi` expose a `SERIES` constant + a plain `#series` reader (safe — `::Pubid::Identifier` declares no `series` attribute and nothing generic reads `.series`) and `build_subseries` stops passing it. The whole serialized vocabulary is now **three keys**: `_type` + `number` always, `version` only on a versioned draft — a sub-series row (`{_type, number}`) has the same shape as an RFC's. **The attributes live on the five LEAVES, not the shared `Pubid::Ietf::Identifier`**, via the `Identifiers::Serialization` mixin (`number` + the `key_value` block; `InternetDraft` merges its own `version` mapping on top — lutaml combines a mixin-installed block with a second in-class one, the ITU precedent). This is the IEEE `number` determinism landmine: a `:string` redefinition of the parent's `Components::Code number` on a class the leaves *inherit* from resolves nondeterministically under multi-flavor load. It was invisible while drafts left `number` nil; it is load-bearing now. The base deliberately carries **no** `key_value` block — one there would be inherited-and-merged by every leaf. `spec/pubid/ietf/root_number_spec.rb` is the tripwire (asserts `number` is a `String`, the exact keys, the exact `to_hash` key sets) and **must run under the full `rake` suite** to mean anything. **(4) Round-trip is validated here because relaton structurally cannot**: `FileIO#id_supported?` **skips** its `to_hash`/`from_hash` check whenever the object is a concrete subclass (`return true unless obj.instance_of?(@pubid_class)`), and *every* IETF id is a subclass — so a lossy round-trip would pass validation silently and produce wrong lookups. Three layers: `fixtures_spec.rb` is now **zero-failure** (was a 90% threshold — wrong for an all-or-nothing index) and checks `to_s` byte-exactness, `from_hash(to_hash)` fidelity in both `to_s` and `to_hash`, **and** a non-empty `root.number` for every fixture line; the fixtures grew to ~2,500 lines (the complete 365-row sub-series corpus, a 139-id structural edge set, a deterministic 1-in-84 draft sample — each file's header records the regeneration command); and `spec/pubid/ietf/corpus_round_trip_spec.rb` runs the same four checks over all 176,862 published ids, **opt-in** via `PUBID_IETF_CORPUS=<dir with the three relaton-data checkouts> bundle exec rake test:corpus_ietf` (self-skips otherwise; ~100s; verified 0 failures). **relaton note — the indexes must be regenerated, there is no alias**: `name` and `series` are gone as serialized keys, so a pre-`number` row (`{_type, name, version}`) deserializes with **`number` nil** and no error at all — lutaml ignores unknown keys, and relaton's subclass skip means `id_supported?` won't catch it either, leaving every draft keyed under `""`. Rendering is therefore the loud failure: `Renderer#require_number!` raises `ArgumentError` on a nil/empty `number` rather than returning `nil` or `"-02"`, and the renderer's sub-series arm is an explicit `when Bcp, Std, Fyi` with an `else raise` instead of a catch-all (a bare `Pubid::Ietf::Identifier` has no `series` reader). Pinned by the "legacy `name`-shaped hash" block in `root_number_spec.rb`. **Pre-existing bug fixed in passing**: `UrnParser`'s bare `Errors::ParseError` does not resolve — `Errors` lives under `Pubid::UrnParser` (the module), which is neither in the lexical scope of `Pubid::Ietf::UrnParser` nor an ancestor of `Base` — so an invalid URN raised `NameError` instead, and the spec's `raise_error(StandardError)` accepted it. Now fully qualified as `Pubid::UrnParser::Errors::ParseError` (the adobe/easc/gost/iala convention) with the spec asserting that exact class. **`Pubid::Iec::UrnParser` has the identical bug and is untouched here.** Also: `Identifier.parse` gained the inline `MAX_INPUT_LENGTH` guard — relaton reaches that class-level funnel directly through `pubid_class:`, bypassing `Pubid::Ietf.parse` — and IETF was added to the three cross-flavor tables it was missing from (`identifier_roundtrip_spec`, `uniform_identifier_handle_spec`, `redos_guard_spec`), which is what exercises the leaf `number` under real multi-flavor load. (hand-off: ietf-index-readiness.)
|
|
8
|
+
|
|
9
|
+
- **`IETF` is a prefix (pubid#644)**: `PREFIXES` gained the publisher token itself. `strip_prefix` (`lib/pubid/ietf/identifiers/base.rb`) already parsed the external spellings `IETF RFC 8341` / `IETF I-D draft-…`, and `to_s(with_publisher: true)` prints that same `IETF `-prefixed form — but without "IETF" in the prefix table the flavor had **no claim** on the string, and the router's non-exact fallback (`IETF RFC 2119` normalizes to the canonical `RFC 2119`) is reserved for prefix owners. So `Pubid.parse("IETF RFC 2119")` raised while `Pubid::Ietf.parse` succeeded. With the prefix registered, the router routes it (fallback path) and the `with_publisher: true` render reproduces the input byte for byte. Bare `RFC 2119` keeps routing exactly as before (exact round trip). Locked by `spec/pubid/router_printed_forms_spec.rb`.
|
data/lib/pubid/ietf.rb
CHANGED
|
@@ -11,8 +11,11 @@ module Pubid
|
|
|
11
11
|
extend Pubid::PrefixesSupport
|
|
12
12
|
|
|
13
13
|
# Leading tokens a printed IETF id can begin with. "draft" covers the
|
|
14
|
-
# "draft-<slug>" Internet-Draft form.
|
|
15
|
-
|
|
14
|
+
# "draft-<slug>" Internet-Draft form. "IETF" covers the external
|
|
15
|
+
# publisher-prefixed spelling ("IETF RFC 2119", "IETF I-D draft-…") that
|
|
16
|
+
# the flavor's own strip_prefix parses and `to_s(with_publisher: true)`
|
|
17
|
+
# prints back. No joint/co-publication prefixes.
|
|
18
|
+
PREFIXES = ["IETF", "RFC", "BCP", "STD", "FYI", "draft"].freeze
|
|
16
19
|
|
|
17
20
|
autoload :Builder, "#{__dir__}/ietf/builder"
|
|
18
21
|
autoload :Identifier, "#{__dir__}/ietf/identifiers/base"
|
data/lib/pubid/iho/CLAUDE.md
CHANGED
|
@@ -1,98 +1,174 @@
|
|
|
1
1
|
# IHO flavor notes
|
|
2
2
|
|
|
3
3
|
IHO (International Hydrographic Organization) identifiers: the
|
|
4
|
-
S/P/M/B/C letter series, their parg-driven parse, and the
|
|
5
|
-
|
|
4
|
+
S/P/M/B/C letter series, their parg-driven parse, and the full
|
|
5
|
+
component model (edition, supplement, part, annex, appendix +
|
|
6
|
+
subnumber, volume, issue, with-year qualifier) from the owner rulings
|
|
7
|
+
of 2026-11-03.
|
|
6
8
|
|
|
7
9
|
Read this before you change `lib/pubid/iho/` or `spec/pubid/iho/`. The
|
|
8
10
|
root `CLAUDE.md` keeps the cross-flavor contract that every flavor
|
|
9
11
|
obeys.
|
|
10
12
|
|
|
11
|
-
## Parsing runs on the parg artifact
|
|
13
|
+
## Parsing runs on the parg artifact, through typed bindings
|
|
12
14
|
|
|
13
15
|
There is no `parser.rb` and no Parslet anywhere in this flavor: parse
|
|
14
|
-
goes `Pubid::Parg::
|
|
15
|
-
`
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
the artifact
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
The
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
`
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
`
|
|
53
|
-
the
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
(`
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
16
|
+
goes `Pubid::Parg::Model.bound(:iho, ...)` — the artifact's
|
|
17
|
+
`bindings identifier` block delivers every component as an
|
|
18
|
+
attribute-named scalar, and `Builder` only dispatches the series
|
|
19
|
+
letter to its concrete class and pads one grammar-level alias (the
|
|
20
|
+
S-100 `v1`/`v5.1` shorthand fills `edition` as `1.0.0`/`5.1.0`). The
|
|
21
|
+
artifact is baked from `grammars/iho.parg` in pubid-grammar; the
|
|
22
|
+
vendored twin `data/parg/iho.parg` is the embedded source's provenance
|
|
23
|
+
copy and must move together with `data/parg/iho.json` (the ISO
|
|
24
|
+
pattern).
|
|
25
|
+
|
|
26
|
+
**Bake pin:** the artifact must be baked at parsanol 1.3.126 (the
|
|
27
|
+
grammar repo's `scripts/bake-one.rb` forces the gem version and
|
|
28
|
+
verifies the `compiler_version` stamp). 1.3.127 is broken fleet-wide
|
|
29
|
+
(parsanol-ruby#434 — its loud-loss check matches the binding PATH
|
|
30
|
+
instead of the CAPTURE name), so both repos pin below it: the grammar
|
|
31
|
+
repo's CI moved 1.3.121 → 1.3.126 with the component-model PR.
|
|
32
|
+
|
|
33
|
+
## The component model (owner rulings, 2026-11-03)
|
|
34
|
+
|
|
35
|
+
Every component is a FIRST-CLASS SEMANTIC component: a typed `:string`
|
|
36
|
+
attribute, independently addressable, URN-encodable and matchable —
|
|
37
|
+
never a string suffix. All of them live flat on `Pubid::Iho::Identifier`
|
|
38
|
+
(mapped through the flavor `key_value` block, no converters):
|
|
39
|
+
|
|
40
|
+
| component | attribute | canonical face | parse-only aliases |
|
|
41
|
+
|-------------|--------------------|-------------------|------------------------------|
|
|
42
|
+
| edition | `edition` | `1.0.0` (bare) | `v1`/`v5.1` (padded) |
|
|
43
|
+
| supplement | `supplement` | `Sup 1` | `Suppl 1` |
|
|
44
|
+
| part | `part` | `Part A`/`Part 1`/`Part S-102` | `S-5-B`, `S-100-1`, `S-158-S-102` (compact) |
|
|
45
|
+
| annex | `annex` | `Annex A` | — |
|
|
46
|
+
| appendix | `appendix` | `Appendix D` | `App`, `Ap.` |
|
|
47
|
+
| subnumber | `appendix_subnumber` | `-2` (joined) | — |
|
|
48
|
+
| volume | `volume` | `Vol. 30`/`Vol. 2024` | `/21` (slash chain), bare `2024` |
|
|
49
|
+
| issue | `issue` | `(2)` | `Issue 2` |
|
|
50
|
+
| year (qualifier) | `year` | `(2024)` | — |
|
|
51
|
+
| component edition | `<kind>_edition` (+ `<kind>_year`) | `<kind> x.y.z (2024)` | — |
|
|
52
|
+
|
|
53
|
+
- **Two main syntaxes:** `IHO S-100 1.0.0` (dated = edition-specific)
|
|
54
|
+
and `IHO S-100` (undated = edition-non-specific). The official IHO
|
|
55
|
+
edition is the dotted x.y.z WITHOUT a year; a year beside an edition
|
|
56
|
+
is the with-year qualifier, never a year-keyed docidentifier. The
|
|
57
|
+
canonical part face is edition-before-part: `IHO S-100 1.0.0 Part 2`.
|
|
58
|
+
- **The part's compact face is hyphenated** (`IHO S-5-B`,
|
|
59
|
+
`IHO S-100-1`) — it parses AND renders via `to_s(compact: true)`.
|
|
60
|
+
Gluing without a separator is disallowed as a form: it cannot extend
|
|
61
|
+
to numbered parts unambiguously (`S-1001`). The four lettered glued
|
|
62
|
+
bases in the records (S-5A, S-5B, S-8A, S-8B) are LEGACY parse-only
|
|
63
|
+
aliases, never rendered; the owner rules later whether to reject
|
|
64
|
+
them. The dash-digit suffix that used to join the number (`P-6-3` as
|
|
65
|
+
number "6-3") is now the part component.
|
|
66
|
+
- **One serial locator: the volume.** P-1 and P-6 are volume-keyed;
|
|
67
|
+
the yearbook year IS a volume (`IHO P-5 Vol. 2024`, short
|
|
68
|
+
`IHO P-5/2024`). The bare year (`IHO P-5 2024`) is a legacy alias
|
|
69
|
+
that binds INTO `volume` — `year` never carries a serial locator.
|
|
70
|
+
- **The with-year qualifier** (`IHO S-100 5.0.0 Part 2 (2024)`) is the
|
|
71
|
+
only base `year` face, always parenthesized beside an edition.
|
|
72
|
+
- **Component-edition duality** (owner: "this can happen with any
|
|
73
|
+
independently publishable artifacts"): supplement, part, annex,
|
|
74
|
+
appendix and volume each carry an optional `<kind>_edition` (+
|
|
75
|
+
`<kind>_year`) of their own, MUTUALLY EXCLUSIVE with the base edition
|
|
76
|
+
context — within-edition (`IHO P-6 1.0.0 Vol. 2`, the component rides
|
|
77
|
+
the base's edition) vs separately-published (`IHO P-6 Vol. 2 1.0.1`,
|
|
78
|
+
`IHO S-98 Part A 1.0.0`, `IHO S-158 Part S-102 0.2.0`; the base is
|
|
79
|
+
cited edition-less). The builder raises when both appear. Every
|
|
80
|
+
trailing edition now follows a component, so the historical
|
|
81
|
+
edition-last spellings reinterpret as component editions.
|
|
82
|
+
- **Series-reference parts**: a check standard applied to another IHO
|
|
83
|
+
series cites it as its part — `IHO S-158 0.2.0 Part S-102`, compact
|
|
84
|
+
`IHO S-158-S-102` (the part value vocabulary is letter | number |
|
|
85
|
+
`[SPMBC]-<digits>`). The wild glued `S-158-102` stays a plain number
|
|
86
|
+
part: the syntax cannot distinguish it from `S-100-1` = Part 1, and
|
|
87
|
+
normalizing it needs an IHO series table (owner call pending).
|
|
88
|
+
- **Supplement canonical face is `Sup`** (the M-2 spelling — why save
|
|
89
|
+
two characters when Annex and Appendix stay full-length); `Suppl`
|
|
90
|
+
(the S-66 wild spelling) is the parse-only alias, never rendered.
|
|
91
|
+
- **Appendix canonical face is `Appendix`**; the subnumber keeps its
|
|
92
|
+
own component (`Appendix D` + `appendix_subnumber` "2" render joined
|
|
93
|
+
as `Appendix D-2`).
|
|
94
|
+
|
|
95
|
+
The compact face (`compact: true`) folds the code-adjacent components
|
|
96
|
+
onto the code: supplement and part hyphenated, volume as the slash
|
|
97
|
+
chain — `IHO M-2-2`, `IHO S-100-2 5.0.0`, `IHO P-5/2024`.
|
|
98
|
+
|
|
99
|
+
## Identity semantics
|
|
100
|
+
|
|
101
|
+
- The components are identity-bearing in `==`/`matches?`; a bare
|
|
102
|
+
reference (`IHO P-5`) leaves them all nil and wildcards the series
|
|
103
|
+
(`spec/pubid/partial_ref_spec.rb` locks the omitted-key set:
|
|
104
|
+
edition/year/volume/issue).
|
|
105
|
+
- Scalar **clusters**: `exclude(:volume)` nils `issue` with it, and
|
|
106
|
+
`exclude(:appendix)` nils `appendix_subnumber` — a reference that
|
|
107
|
+
omits the finer component still matches a hit that carries one (the
|
|
108
|
+
base's additive `:year -> :date` alias shape).
|
|
109
|
+
- `all_parts_edition_keys` needs no override: the default
|
|
110
|
+
`%i[date year edition version]` already covers `:year` and
|
|
111
|
+
`:edition`.
|
|
112
|
+
|
|
113
|
+
## URN scheme — positional, containment-ordered
|
|
114
|
+
|
|
115
|
+
The URN mirrors the containment tree, one semantic atom per segment;
|
|
116
|
+
the segment grammar table lives in the grammar header
|
|
117
|
+
(`grammars/iho.parg`) and is implemented by
|
|
118
|
+
`urn_generator.rb`/`urn_parser.rb`:
|
|
119
|
+
|
|
120
|
+
urn:iho:<type>:<number>[:ed-<x.y.z>][:sup-<n>][:part-<X>]
|
|
121
|
+
[:annex-<A>][:appendix-<B>][:<sub>][:vol-<n>][:issue-<n>]
|
|
122
|
+
[:year-<yyyy>]
|
|
123
|
+
|
|
124
|
+
- Markers are the lowercased face tokens (`ed-` follows ISO's
|
|
125
|
+
RFC 5141 precedent); letters are case-preserved.
|
|
126
|
+
- `ed-` and `year-` are POSITIONAL: the edition attaches to the node it
|
|
127
|
+
follows — the base when no component precedes it, else that
|
|
128
|
+
component's own edition (`vol-2:ed-1.0.1` = separately-published
|
|
129
|
+
volume). A `year-` belongs to a component only when it follows that
|
|
130
|
+
component's own edition; otherwise it is the base with-year
|
|
131
|
+
qualifier.
|
|
132
|
+
- Containment: part ⊃ annex ⊃ appendix ⊃ subnumber (bare digit atom,
|
|
133
|
+
only directly after its appendix); vol ⊃ issue.
|
|
134
|
+
- The number occupies every leading bare segment up to the first
|
|
135
|
+
marker, so a colon number (`S-158:100` → `urn:iho:s:158:100`)
|
|
136
|
+
round-trips without escaping. A colon number and a volume stay
|
|
137
|
+
distinct because the volume is marker-tagged (`urn:iho:s:158:2024`
|
|
138
|
+
vs `urn:iho:s:158:vol-2024` — the #637 hazard, kept under the
|
|
139
|
+
positional scheme).
|
|
140
|
+
- The yearbook year emits as the volume (`urn:iho:p:5:vol-2024`);
|
|
141
|
+
`year-` exists only for the standards qualifier.
|
|
142
|
+
- The parser validates every marker value against its own pattern and
|
|
143
|
+
rejects unknown/reordered/duplicate/junk segments with
|
|
144
|
+
`Pubid::Errors::UrnParseError` (the ISO urn_parser strictness); it
|
|
145
|
+
builds through `Builder.build` from the decoded attributes, so URN
|
|
146
|
+
decode and string parse share one construction path.
|
|
147
|
+
|
|
148
|
+
**Breaking URN migrations this scheme carries** (legacy forms stay
|
|
149
|
+
decodable, never emitted): the dotted keys (`part.4a`, `suppl.1`,
|
|
150
|
+
`ap.A`, `annex.A`, `vol.30`, `issue.2`, `appendix.D-2`) and the bare
|
|
151
|
+
a.b.c edition segment move to the marker segments; the #635/#637
|
|
152
|
+
`y.2024` tag decodes into the **volume** (`urn:iho:p:5:y.2024` now
|
|
153
|
+
re-emits as `urn:iho:p:5:vol-2024`). relaton-data-iho needs a re-crawl
|
|
154
|
+
(it is already legacy pre-pubid-2 shape); the frozen testsuite corpus
|
|
155
|
+
rows are re-frozen in the coupled corpus round.
|
|
80
156
|
|
|
81
157
|
## Fixtures run through the classify pipeline
|
|
82
158
|
|
|
83
159
|
`spec/fixtures/iho/identifiers/full/identifiers.txt` is the input
|
|
84
|
-
corpus
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
160
|
+
corpus (seeded from the hand-curated pass files, the #407 serial
|
|
161
|
+
forms and the component-model matrix off the IHO publications
|
|
162
|
+
spreadsheet); `rake "validation:classify[iho]"` regenerates `pass/`
|
|
163
|
+
and `fail/` from it (the classifier deletes pass/fail first — never
|
|
164
|
+
edit them by hand, edit `full/` and reclassify). The normalizing
|
|
165
|
+
parses (Suppl→Sup, Ap.→Appendix, glued→expanded, bare year→Vol.,
|
|
166
|
+
v-shorthand→dotted) are recorded as `!input!rendered` entries.
|
|
91
167
|
|
|
92
168
|
## relaton-side follow-ups (not this repo)
|
|
93
169
|
|
|
94
|
-
|
|
95
|
-
`
|
|
96
|
-
|
|
97
|
-
`
|
|
98
|
-
in step with the relaton-data-iho re-key.
|
|
170
|
+
`Relaton::Iho::Bibliography#pubid_match?`/`version_key` need the
|
|
171
|
+
edition rename (the serialized key moved `version` → `edition`) and
|
|
172
|
+
the volume unification; relaton-cli's `MachineIndex::REFINABLE_KEYS`
|
|
173
|
+
needs `edition`/`volume` beside the retired `version`/`year` keys; the
|
|
174
|
+
pubid pin moves in step with the relaton-data-iho re-key.
|
data/lib/pubid/iho/builder.rb
CHANGED
|
@@ -2,25 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
module Pubid
|
|
4
4
|
module Iho
|
|
5
|
-
# Build IHO identifier objects from the
|
|
5
|
+
# Build IHO identifier objects from the grammar's typed bound map.
|
|
6
|
+
#
|
|
7
|
+
# Parse goes through {Pubid::Parg::Model.bound} — the artifact's
|
|
8
|
+
# `bindings identifier` block delivers every component as an
|
|
9
|
+
# attribute-named scalar (type, number, part, annex, appendix,
|
|
10
|
+
# appendix_subnumber, supplement, edition, edition_shorthand, year,
|
|
11
|
+
# volume, issue) — so the builder only dispatches the series letter
|
|
12
|
+
# to its concrete class and resolves the one grammar-level alias:
|
|
13
|
+
# the S-100 "v1"/"v5.1" edition shorthand pads to the official
|
|
14
|
+
# dotted x.y.z edition ("1" -> "1.0.0", "5.1" -> "5.1.0").
|
|
6
15
|
class Builder
|
|
16
|
+
ATTRIBUTES = %i[number appendix appendix_subnumber part annex
|
|
17
|
+
supplement year volume issue
|
|
18
|
+
part_edition part_year supplement_edition
|
|
19
|
+
supplement_year annex_edition annex_year
|
|
20
|
+
appendix_edition appendix_year volume_edition
|
|
21
|
+
volume_year].freeze
|
|
22
|
+
# The independently-publishable components' own editions; the
|
|
23
|
+
# base edition context is mutually exclusive with them (owner
|
|
24
|
+
# ruling on the two publication modes).
|
|
25
|
+
COMPONENT_EDITIONS = %i[part_edition supplement_edition annex_edition
|
|
26
|
+
appendix_edition volume_edition].freeze
|
|
27
|
+
|
|
7
28
|
def build(parsed)
|
|
8
29
|
hash = parsed.is_a?(Array) ? parsed.reduce({}, :merge) : parsed
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
appendix: stringify(hash[:appendix]),
|
|
14
|
-
part: stringify(hash[:part]),
|
|
15
|
-
annex: stringify(hash[:annex]),
|
|
16
|
-
supplement: stringify(hash[:supplement]),
|
|
17
|
-
version: stringify(hash[:version]),
|
|
18
|
-
year: stringify(hash[:year]),
|
|
19
|
-
volume: stringify(hash[:volume]),
|
|
20
|
-
issue: stringify(hash[:issue]),
|
|
21
|
-
}.compact
|
|
22
|
-
|
|
23
|
-
Pubid::Iho.identifier_klass_for_type_letter(type_letter).new(**attrs)
|
|
30
|
+
attrs = stringified_attrs(hash)
|
|
31
|
+
assert_edition_mode!(attrs)
|
|
32
|
+
klass = Pubid::Iho.identifier_klass_for_type_letter(stringify(hash[:type]))
|
|
33
|
+
klass.new(**attrs)
|
|
24
34
|
end
|
|
25
35
|
|
|
26
36
|
def self.build(parsed)
|
|
@@ -29,6 +39,41 @@ module Pubid
|
|
|
29
39
|
|
|
30
40
|
private
|
|
31
41
|
|
|
42
|
+
# The two publication modes are mutually exclusive: an identifier
|
|
43
|
+
# is either within-edition (a base edition its components ride) or
|
|
44
|
+
# separately-published (a component carries its own edition and
|
|
45
|
+
# the base is cited edition-less).
|
|
46
|
+
def assert_edition_mode!(attrs)
|
|
47
|
+
return unless attrs[:edition]
|
|
48
|
+
return unless COMPONENT_EDITIONS.any? { |key| attrs[key] }
|
|
49
|
+
|
|
50
|
+
owned = attrs.slice(*COMPONENT_EDITIONS).compact.keys.join(", ")
|
|
51
|
+
raise ArgumentError,
|
|
52
|
+
"a base edition and a component edition are mutually " \
|
|
53
|
+
"exclusive: #{owned} cannot ride edition #{attrs[:edition]}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def stringified_attrs(hash)
|
|
57
|
+
attrs = ATTRIBUTES.to_h { |key| [key, stringify(hash[key])] }
|
|
58
|
+
attrs[:edition] = edition_from(hash[:edition], hash[:edition_shorthand])
|
|
59
|
+
attrs.compact
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The official IHO edition is the dotted x.y.z; the "v1"/"v5.1"
|
|
63
|
+
# shorthand is a parse-only alias that fills the same attribute.
|
|
64
|
+
def edition_from(edition, shorthand)
|
|
65
|
+
return stringify(edition) if edition
|
|
66
|
+
|
|
67
|
+
pad_edition_shorthand(shorthand)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def pad_edition_shorthand(shorthand)
|
|
71
|
+
return nil if shorthand.nil?
|
|
72
|
+
|
|
73
|
+
major, minor = stringify(shorthand).split(".")
|
|
74
|
+
"#{major}.#{minor || '0'}.0"
|
|
75
|
+
end
|
|
76
|
+
|
|
32
77
|
def stringify(value)
|
|
33
78
|
return nil if value.nil?
|
|
34
79
|
|
data/lib/pubid/iho/identifier.rb
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
# The IHO flavor base class is Pubid::Iho::Identifier — a real Pubid::Identifier
|
|
4
4
|
# subclass that every concrete IHO identifier descends from, so `is_a?` identity
|
|
5
5
|
# and the shared polymorphic `from_hash` work natively (no facade needed). Its
|
|
6
|
-
# body and `.parse` live in identifiers/base.rb; this file just ensures
|
|
7
|
-
# references Pubid::Iho::Identifier directly.
|
|
6
|
+
# body and `.parse` live in identifiers/base.rb; this file just ensures
|
|
7
|
+
# it is loaded when a consumer references Pubid::Iho::Identifier directly.
|