pubid 2.0.20 → 2.0.22
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 +73 -31
- data/data/iec/legacy-notation.md +298 -0
- data/data/iho/s4-reserved-specs.yaml +15 -0
- data/data/iso/historic-schemes.md +368 -0
- data/data/parg/bipm.json +1 -1
- data/data/parg/bipm.parg +46 -11
- data/data/parg/iec.json +1 -1
- data/data/parg/iho.json +1 -1
- data/data/parg/iho.parg +128 -11
- data/data/parg/iso.json +1 -1
- data/data/parg/iso.parg +142 -2
- data/data/parg/plateau.json +1 -1
- data/data/parg/tables/iec_typed_stages.yaml +3 -0
- data/data/parg/tables/typed_stages_ballots.yaml +6 -0
- data/data/parg/tables/typed_stages_supplements.yaml +1 -0
- data/lib/pubid/bipm/CLAUDE.md +6 -1
- data/lib/pubid/bipm/builder.rb +20 -1
- data/lib/pubid/bipm/renderer.rb +14 -2
- data/lib/pubid/bipm/urn_generator.rb +80 -14
- data/lib/pubid/bipm/urn_parser.rb +70 -6
- data/lib/pubid/ecma/CLAUDE.md +12 -0
- data/lib/pubid/ecma/identifier.rb +1 -2
- data/lib/pubid/ecma/parser.rb +49 -0
- data/lib/pubid/iec/CLAUDE.md +9 -1
- data/lib/pubid/iec/builder.rb +47 -0
- data/lib/pubid/iec/identifier.rb +19 -70
- data/lib/pubid/iec/identifiers/amendment.rb +34 -0
- data/lib/pubid/iec/identifiers/corrigendum.rb +6 -1
- data/lib/pubid/iec/identifiers/fragment_identifier.rb +3 -3
- data/lib/pubid/iec/single_identifier.rb +69 -4
- data/lib/pubid/iec/supplement_identifier.rb +89 -83
- data/lib/pubid/iho/CLAUDE.md +54 -1
- data/lib/pubid/iho/builder.rb +105 -2
- data/lib/pubid/iho/identifiers/base.rb +52 -1
- data/lib/pubid/iho/pubid_urn_schema.rb +12 -4
- data/lib/pubid/iho/renderer.rb +39 -2
- data/lib/pubid/iho/reserved_specs.rb +54 -0
- data/lib/pubid/iho/urn_generator.rb +6 -0
- data/lib/pubid/iho/urn_parser.rb +50 -9
- data/lib/pubid/iho.rb +1 -0
- data/lib/pubid/iso/CLAUDE.md +49 -0
- data/lib/pubid/iso/builder.rb +77 -0
- data/lib/pubid/iso/identifiers/amendment.rb +1 -1
- data/lib/pubid/iso/single_identifier.rb +7 -1
- data/lib/pubid/nist/CLAUDE.md +49 -0
- data/lib/pubid/nist/builder.rb +5 -0
- data/lib/pubid/nist/identifiers/base.rb +42 -1
- data/lib/pubid/nist/identifiers/federal_information_processing_standards.rb +44 -5
- data/lib/pubid/nist/preprocessor.rb +18 -0
- data/lib/pubid/nist/urn_parser.rb +1 -1
- data/lib/pubid/plateau/CLAUDE.md +35 -0
- data/lib/pubid/plateau/builder.rb +4 -0
- data/lib/pubid/plateau/identifiers/handbook.rb +10 -2
- data/lib/pubid/plateau/pubid_urn_schema.rb +1 -0
- data/lib/pubid/plateau/renderer.rb +6 -1
- data/lib/pubid/sae/CLAUDE.md +11 -2
- data/lib/pubid/version.rb +1 -1
- metadata +7 -2
|
@@ -31,6 +31,7 @@ module Pubid
|
|
|
31
31
|
# - urn:iho:m:2:sup-1 IHO M-2 Sup 1
|
|
32
32
|
# - urn:iho:p:5:vol-2024 IHO P-5 Vol. 2024
|
|
33
33
|
# - urn:iho:p:1:vol-30:issue-2 IHO P-1 Vol. 30 (2)
|
|
34
|
+
# - urn:iho:s:4:ed-4.10.0:part-B:spec-401 IHO S-4 4.10.0 B-401
|
|
34
35
|
class UrnGenerator
|
|
35
36
|
attr_reader :identifier
|
|
36
37
|
|
|
@@ -49,6 +50,11 @@ module Pubid
|
|
|
49
50
|
[:supplement, "sup"], [:supplement_edition, "ed"],
|
|
50
51
|
[:supplement_year, "year"],
|
|
51
52
|
[:part, "part"], [:part_edition, "ed"], [:part_year, "year"],
|
|
53
|
+
# the S-4 spec citation: a child of its part (the citation
|
|
54
|
+
# letter rides the part- atom), the dotted sub and the
|
|
55
|
+
# parenthesized variant its own children. The SECTION is NOT
|
|
56
|
+
# emitted — derivable from the spec's hundreds digit.
|
|
57
|
+
[:spec, "spec"], [:spec_sub, "specsub"], [:spec_variant, "specvar"],
|
|
52
58
|
[:annex, "annex"], [:annex_edition, "ed"], [:annex_year, "year"],
|
|
53
59
|
[:appendix, "appendix"], [:appendix_subnumber, nil],
|
|
54
60
|
[:appendix_edition, "ed"], [:appendix_year, "year"],
|
data/lib/pubid/iho/urn_parser.rb
CHANGED
|
@@ -7,8 +7,8 @@ module Pubid
|
|
|
7
7
|
# the segment table lives in the grammar header and the flavor notes):
|
|
8
8
|
#
|
|
9
9
|
# urn:iho:<type>:<number>[:ed-<x.y.z>][:sup-<n>][:part-<X>]
|
|
10
|
-
# [:
|
|
11
|
-
# [:year-<yyyy>]
|
|
10
|
+
# [:spec-<n>[:specsub-<n>][:specvar-<X>]][:annex-<A>]
|
|
11
|
+
# [:appendix-<B>][:<sub>][:vol-<n>][:issue-<n>][:year-<yyyy>]
|
|
12
12
|
#
|
|
13
13
|
# Segment discipline (mirrors the ISO URN parser's strictness):
|
|
14
14
|
# - the docnumber occupies every leading bare number segment up to
|
|
@@ -22,7 +22,11 @@ module Pubid
|
|
|
22
22
|
# directly after its appendix segment (a year is always
|
|
23
23
|
# marker-tagged, so a bare digit there can only be the subnumber);
|
|
24
24
|
# - the issue is allowed only beside its volume and validated as
|
|
25
|
-
# digits
|
|
25
|
+
# digits;
|
|
26
|
+
# - the S-4 spec atom is a child of its part (the citation letter
|
|
27
|
+
# rides the part- atom): specsub-/specvar- only after spec-, and
|
|
28
|
+
# no ed-/year- after a spec node (the spec rides its publication's
|
|
29
|
+
# edition; the section is derivable, never emitted).
|
|
26
30
|
#
|
|
27
31
|
# Legacy 2.0.x spellings remain decodable but are never emitted:
|
|
28
32
|
# the bare a.b.c edition segment, the dotted keys part./annex./
|
|
@@ -38,6 +42,7 @@ module Pubid
|
|
|
38
42
|
# - urn:iho:p:5:vol-2024 → IHO P-5 Vol. 2024
|
|
39
43
|
# - urn:iho:p:1:vol-30:issue-2 → IHO P-1 Vol. 30 (2)
|
|
40
44
|
# - urn:iho:s:158:100 → IHO S-158:100
|
|
45
|
+
# - urn:iho:s:4:ed-4.10.0:part-B:spec-401 → IHO S-4 4.10.0 B-401
|
|
41
46
|
class UrnParser < Pubid::UrnParser::Base
|
|
42
47
|
TYPE_LETTERS = %w[s p m b c].freeze
|
|
43
48
|
|
|
@@ -50,6 +55,9 @@ module Pubid
|
|
|
50
55
|
"ed" => [:edition, /\A\d+\.\d+\.\d+\z/],
|
|
51
56
|
"sup" => [:supplement, /\A\d+\z/],
|
|
52
57
|
"part" => [:part, /\A(?:\d+[a-zA-Z]*|[A-Z]|[SPMBC]-\d+)\z/],
|
|
58
|
+
"spec" => [:spec, /\A[1-6]\d\d\z/],
|
|
59
|
+
"specsub" => [:spec_sub, /\A\d+\z/],
|
|
60
|
+
"specvar" => [:spec_variant, /\A[A-Z]+\z/],
|
|
53
61
|
"annex" => [:annex, /\A[A-Z]\z/],
|
|
54
62
|
"appendix" => [:appendix, /\A(?:[A-Z]|\d+)\z/],
|
|
55
63
|
"vol" => [:volume, /\A\d+\z/],
|
|
@@ -63,6 +71,12 @@ module Pubid
|
|
|
63
71
|
# follow (the component-edition duality).
|
|
64
72
|
POSITIONAL_MARKERS = %i[edition year].freeze
|
|
65
73
|
|
|
74
|
+
# The S-4 spec node and its children: specsub-/specvar- are only
|
|
75
|
+
# allowed after their parent, and an S-4 spec rides its
|
|
76
|
+
# publication's edition — no ed-/year- inside a spec.
|
|
77
|
+
SPEC_NODES = %i[spec spec_sub spec_variant].freeze
|
|
78
|
+
SPEC_PARENTS = %i[spec spec_sub].freeze
|
|
79
|
+
|
|
66
80
|
# Legacy dotted keys (deprecated; decodable, never emitted).
|
|
67
81
|
# "ap" is the pre-positional appendix tag; "suppl" the pre-positional
|
|
68
82
|
# supplement tag; "y" the pre-positional year tag — under the
|
|
@@ -97,11 +111,22 @@ module Pubid
|
|
|
97
111
|
# own edition).
|
|
98
112
|
ctx = { component: nil, edition_owner: nil }
|
|
99
113
|
consume_segments!(attrs, segments, ctx, urn)
|
|
100
|
-
|
|
114
|
+
build_identifier({ type: type_token.upcase, **attrs }, urn)
|
|
101
115
|
end
|
|
102
116
|
|
|
103
117
|
private
|
|
104
118
|
|
|
119
|
+
# The semantic validations the builder applies to the decoded shape
|
|
120
|
+
# (the S-4 anchor, the citation letter) are URN validation at this
|
|
121
|
+
# layer: a URN that asserts a citation it cannot carry is a
|
|
122
|
+
# malformed URN, and the raw ArgumentError stays as the cause.
|
|
123
|
+
def build_identifier(hash, urn)
|
|
124
|
+
Builder.build(hash)
|
|
125
|
+
rescue ArgumentError => e
|
|
126
|
+
raise Pubid::Errors::UrnParseError,
|
|
127
|
+
"#{e.message} in #{urn.inspect}"
|
|
128
|
+
end
|
|
129
|
+
|
|
105
130
|
def read_type!(segments, urn)
|
|
106
131
|
type_token = segments.shift.to_s.downcase
|
|
107
132
|
return type_token if TYPE_LETTERS.include?(type_token)
|
|
@@ -159,10 +184,9 @@ module Pubid
|
|
|
159
184
|
marker, value = MARKER_RE.match(segment).captures
|
|
160
185
|
key, pattern = MARKERS.fetch(marker)
|
|
161
186
|
raise_invalid!(marker, value, urn) unless value.match?(pattern)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
end
|
|
187
|
+
assert_spec_parent!(key, ctx, urn)
|
|
188
|
+
return consume_positional!(attrs, key, value, ctx, urn) if
|
|
189
|
+
POSITIONAL_MARKERS.include?(key)
|
|
166
190
|
|
|
167
191
|
store!(attrs, key, value, urn)
|
|
168
192
|
ctx[:component] = key
|
|
@@ -170,6 +194,16 @@ module Pubid
|
|
|
170
194
|
consume_subnumber!(attrs, key, segments, ctx, urn)
|
|
171
195
|
end
|
|
172
196
|
|
|
197
|
+
# specsub-/specvar- are children of the spec atom — allowed only
|
|
198
|
+
# in their parent's slot (the SEGMENTS table in the generator).
|
|
199
|
+
def assert_spec_parent!(key, ctx, urn)
|
|
200
|
+
return unless %i[spec_sub spec_variant].include?(key)
|
|
201
|
+
return if SPEC_PARENTS.include?(ctx[:component])
|
|
202
|
+
|
|
203
|
+
raise Pubid::Errors::UrnParseError,
|
|
204
|
+
"#{key}- is only allowed after spec- in #{urn.inspect}"
|
|
205
|
+
end
|
|
206
|
+
|
|
173
207
|
def raise_invalid!(marker, value, urn)
|
|
174
208
|
raise Pubid::Errors::UrnParseError,
|
|
175
209
|
"Invalid #{marker} value #{value.inspect} in #{urn.inspect}"
|
|
@@ -177,8 +211,15 @@ module Pubid
|
|
|
177
211
|
|
|
178
212
|
# ed- and year- are POSITIONAL markers (see the marker docs): the
|
|
179
213
|
# edition attaches to the component it follows, the year to the
|
|
180
|
-
# edition owner.
|
|
214
|
+
# edition owner. An S-4 spec carries neither — it rides its
|
|
215
|
+
# publication's edition.
|
|
181
216
|
def consume_positional!(attrs, key, value, ctx, urn)
|
|
217
|
+
if SPEC_NODES.include?(ctx[:component])
|
|
218
|
+
raise Pubid::Errors::UrnParseError,
|
|
219
|
+
"an S-4 spec rides its publication's edition: #{key}- " \
|
|
220
|
+
"cannot follow a spec segment in #{urn.inspect}"
|
|
221
|
+
end
|
|
222
|
+
|
|
182
223
|
if key == :edition
|
|
183
224
|
return store_component_scoped!(attrs, :edition, value, ctx, urn)
|
|
184
225
|
end
|
data/lib/pubid/iho.rb
CHANGED
|
@@ -12,6 +12,7 @@ module Pubid
|
|
|
12
12
|
autoload :Identifier, "#{__dir__}/iho/identifiers/base"
|
|
13
13
|
autoload :Identifiers, "#{__dir__}/iho/identifiers"
|
|
14
14
|
autoload :Renderer, "#{__dir__}/iho/renderer"
|
|
15
|
+
autoload :ReservedSpecs, "#{__dir__}/iho/reserved_specs"
|
|
15
16
|
autoload :UrnGenerator, "#{__dir__}/iho/urn_generator"
|
|
16
17
|
autoload :UrnParser, "#{__dir__}/iho/urn_parser"
|
|
17
18
|
autoload :PubidUrnSchema, "#{__dir__}/iho/pubid_urn_schema"
|
data/lib/pubid/iso/CLAUDE.md
CHANGED
|
@@ -158,3 +158,52 @@
|
|
|
158
158
|
another reason — `DirectivesSupplement#supplement_publisher` is an
|
|
159
159
|
`Iso::Components::Publisher` after `parse` and a `Pubid::Components::Publisher`
|
|
160
160
|
after `from_hash` (11 supplements, 3 bundles).
|
|
161
|
+
|
|
162
|
+
- **Historic TYPE-first ballot faces decode to the canonical on-base slash
|
|
163
|
+
form (owner ruling 2026-11-12, Option B — IHO-style decode-to-modern;
|
|
164
|
+
SSOT `data/iso/historic-schemes.md`).** The DAM-family token in TYPE
|
|
165
|
+
position (`ISO/IEC FDAM 1 14496-12:2004`, `ISO/PDAM 19973-2`) and the
|
|
166
|
+
space-attached ballot face (`ISO 15877-5:2009 FDAM 1`) used to raise; the
|
|
167
|
+
grammar (v0.5.0) gained two additive branches at the END of the
|
|
168
|
+
identifier alternation, so every face an existing member parses keeps its
|
|
169
|
+
exact parse (replayed: 15,307 fixture inputs and 213,414 stored
|
|
170
|
+
docidentifier faces byte-identical on `to_s`/`to_urn`/`to_hash`/
|
|
171
|
+
`to_mr_string`). What to know before editing either side:
|
|
172
|
+
- **The ballot token captures as `ballot_type_with_stage`** (table
|
|
173
|
+
`typed_stages_ballots`: PDAM, DAM, DAmd, FDAM, FDAmd, FPDAM), NOT as
|
|
174
|
+
`supp_type_with_stage` — a shared name would make the builder's
|
|
175
|
+
supplement-prefix peel wrap a second layer, while the stored twins keep
|
|
176
|
+
ONE level. `Builder#build_ballot_supplement` assembles the single
|
|
177
|
+
amendment layer and nests the rest (prefix, copublishers, the base's
|
|
178
|
+
own `type_with_stage` — the `TR` of `PDAM TR 9573-11` — and
|
|
179
|
+
`number_with_part`/`date`) as `:base`.
|
|
180
|
+
- **`ballot_number` is space-bounded on both sides** (mandatory trailing
|
|
181
|
+
space in the grammar group) so `PDAM 19973-2` cannot read 19973 as the
|
|
182
|
+
ballot number: with no second space the group backtracks and 19973
|
|
183
|
+
stays in `second_part`. This is the base-whole guard — the IEC-flavor
|
|
184
|
+
hazard (#693's `ensure_base`) of a base part migrating into the
|
|
185
|
+
supplement slot cannot happen here; the legacy slash-part of
|
|
186
|
+
`FPDAM 9075/1` stays a base part (`9075-1`).
|
|
187
|
+
- **The number precedence is TYPE token → on-base stamp → implied "1"**;
|
|
188
|
+
the stamp's number/date capture as `ballot_stamp_*` while its token
|
|
189
|
+
letters match UNCAPTURED (Option B keeps no spelling — the ballot's
|
|
190
|
+
stage is the TYPE token's). The implied 1 is the owner's no-number
|
|
191
|
+
rule, applied by `Builder#apply_implied_amendment_number` on EVERY
|
|
192
|
+
builder path (parse, URN, bundle members) for `Identifiers::Amendment`
|
|
193
|
+
with a nil number and `typed_stage.code` ∈ `{pdam, damd, fdamd, fpdam}`
|
|
194
|
+
— by `#code`, NOT `#stage_code` (PDAM's stage_code is `:cd`), and
|
|
195
|
+
`to_sym`-ed (the bound map delivers strings). The published stage
|
|
196
|
+
(abbr `Amd`) is out of scope: a numberless `ISO 1234/Amd` stays nil.
|
|
197
|
+
- **The `Am` spelling** joined `typed_stages_supplements` (after the Amd
|
|
198
|
+
entries) and the published stage's abbr list, so `/Am1:2006` parses as
|
|
199
|
+
an alias of `/Amd 1` through the EXISTING supplement branch and the
|
|
200
|
+
ballot stamp accepts it the same way. `detect_stage_format_long` now
|
|
201
|
+
treats an original spelling that is a prefix of the long abbr as the
|
|
202
|
+
long family, so `Am` renders canonical `Amd`; `AMD` still renders short
|
|
203
|
+
(facets keep spelling on render), `Amd.` still renders `Amd`.
|
|
204
|
+
- **The decode is one-way**: parse-in only, canonical renders emit the
|
|
205
|
+
on-base slash face, and a ballot face == its canonical spelling
|
|
206
|
+
(original_abbr is equality-ignored). Locked by
|
|
207
|
+
`spec/pubid/iso/ballot_face_decode_spec.rb` (all 13 SSOT anchors, the
|
|
208
|
+
no-number rule, base-whole guards, Am alias, stored-twin agreement, and
|
|
209
|
+
the website-face-vs-stored-face drift rows the SSOT §5 records).
|
data/lib/pubid/iso/builder.rb
CHANGED
|
@@ -151,6 +151,16 @@ module Pubid
|
|
|
151
151
|
# reason). The document itself holds no all-parts mark.
|
|
152
152
|
all_parts = parsed_hash.delete(:all_parts)
|
|
153
153
|
|
|
154
|
+
# Historic TYPE-first ballot face ("ISO/IEC FDAM 1 14496-12:2004",
|
|
155
|
+
# "ISO/PDAM 19973-2", "ISO 15877-5:2009 FDAM 1"): the ballot_* keys
|
|
156
|
+
# are one amendment layer over the whole base, never a second
|
|
157
|
+
# supplement layer (the stored twins keep one level), so the layer
|
|
158
|
+
# is assembled here, before the supplement-prefix peel could read
|
|
159
|
+
# the ballot token as an outer wrapper.
|
|
160
|
+
if parsed_hash.key?(:ballot_type_with_stage)
|
|
161
|
+
return build_ballot_supplement(parsed_hash, all_parts)
|
|
162
|
+
end
|
|
163
|
+
|
|
154
164
|
# The copublisher tail captures as one scalar span ("/IEC/IEEE");
|
|
155
165
|
# split it into the {copublisher:} element hashes the merge and
|
|
156
166
|
# the :copublishers cast consume. A pre-built array (the URN
|
|
@@ -261,6 +271,10 @@ module Pubid
|
|
|
261
271
|
identifier.type = default_typed_stage.to_type
|
|
262
272
|
end
|
|
263
273
|
|
|
274
|
+
# The owner's no-number rule: a numberless DAM-family ballot-stage
|
|
275
|
+
# amendment is Amendment 1 (see the method comment).
|
|
276
|
+
apply_implied_amendment_number(identifier)
|
|
277
|
+
|
|
264
278
|
all_parts ? identifier.to_all_parts : identifier
|
|
265
279
|
end
|
|
266
280
|
|
|
@@ -306,6 +320,69 @@ module Pubid
|
|
|
306
320
|
build(inner)
|
|
307
321
|
end
|
|
308
322
|
|
|
323
|
+
# Historic TYPE-first ballot face (owner ruling 2026-11-12, Option B
|
|
324
|
+
# decode-to-modern): "[P|F]?DAM [N] BASE [stamp]" decodes to ONE
|
|
325
|
+
# amendment over the WHOLE base, rendered as the canonical on-base
|
|
326
|
+
# slash face. The stage comes from the ballot token; the number from
|
|
327
|
+
# the TYPE token, then the on-base stamp, then the owner's no-number
|
|
328
|
+
# rule (a ballot face with no number anywhere names Amendment 1 —
|
|
329
|
+
# identity-bearing, so it is set here at build time and serialized,
|
|
330
|
+
# not at render time); the stamp's date is the ballot supplement's
|
|
331
|
+
# date. The base keeps every component it carried: a legacy
|
|
332
|
+
# slash-part stays a part of the base number ("ISO/IEC FPDAM 9075/1"
|
|
333
|
+
# → base "9075-1"), and the base's own type token stays the base's
|
|
334
|
+
# ("ISO/IEC PDAM TR 9573-11" → base "TR 9573-11") — nothing migrates
|
|
335
|
+
# into the supplement slot. The stamp's token letters (Amd/Am/…) are
|
|
336
|
+
# decode-away: the ballot's stage is the TYPE token's.
|
|
337
|
+
#
|
|
338
|
+
# The rest of the hash — prefix, copublishers, the base's
|
|
339
|
+
# type_with_stage / number_with_part / date — is exactly the builder
|
|
340
|
+
# input of the base document, so it nests as :base and re-enters
|
|
341
|
+
# build.
|
|
342
|
+
def build_ballot_supplement(hash, all_parts)
|
|
343
|
+
fields = { type_with_stage: hash.fetch(:ballot_type_with_stage) }
|
|
344
|
+
|
|
345
|
+
number = hash[:ballot_number] ||
|
|
346
|
+
hash[:ballot_stamp_number] ||
|
|
347
|
+
hash[:ballot_stamp_dot_number] ||
|
|
348
|
+
"1"
|
|
349
|
+
fields[:number_with_part] = number
|
|
350
|
+
|
|
351
|
+
if (stamp_date = hash[:ballot_stamp_date])
|
|
352
|
+
fields[:date] = stamp_date
|
|
353
|
+
end
|
|
354
|
+
if hash[:ballot_stamp_undated_marker]
|
|
355
|
+
fields[:undated_marker] = hash[:ballot_stamp_undated_marker]
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
rest = hash.except(:ballot_type_with_stage, :ballot_number,
|
|
359
|
+
:ballot_stamp_number, :ballot_stamp_dot_number,
|
|
360
|
+
:ballot_stamp_date, :ballot_stamp_undated_marker)
|
|
361
|
+
|
|
362
|
+
inner = { **fields, base: rest }
|
|
363
|
+
inner[:all_parts] = all_parts if all_parts
|
|
364
|
+
build(inner)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# The owner's no-number rule, applied on every construction path that
|
|
368
|
+
# routes through this builder (parse, URN, joint/bundle members): a
|
|
369
|
+
# DAM-family ballot-stage amendment token with no number anywhere
|
|
370
|
+
# names Amendment 1 ("ISO 19973-2/PDAM", "ISO/IEC PDAM 12207"). The
|
|
371
|
+
# number is identity-bearing, so the default lands at build time and
|
|
372
|
+
# to_hash serializes it — parse and from_hash(to_hash) agree because
|
|
373
|
+
# both carry the filled number. The ballot stages are matched by
|
|
374
|
+
# TypedStage#code (the per-typed-stage key) — PDAM's #stage_code is
|
|
375
|
+
# :cd, the harmonized range it lives in.
|
|
376
|
+
BALLOT_STAGE_CODES = %i[pdam damd fdamd fpdam].freeze
|
|
377
|
+
|
|
378
|
+
def apply_implied_amendment_number(identifier)
|
|
379
|
+
return unless identifier.is_a?(Identifiers::Amendment)
|
|
380
|
+
return unless identifier.number.nil?
|
|
381
|
+
return unless BALLOT_STAGE_CODES.include?(identifier.typed_stage&.code&.to_sym)
|
|
382
|
+
|
|
383
|
+
identifier.number = "1"
|
|
384
|
+
end
|
|
385
|
+
|
|
309
386
|
def build_directive_supplement(hash, all_parts)
|
|
310
387
|
fields = rekey_prefix(hash.slice(*DSUPP_KEYS), "dsupp_")
|
|
311
388
|
rest = hash.except(*DSUPP_KEYS)
|
|
@@ -36,7 +36,13 @@ module Pubid
|
|
|
36
36
|
return false unless typed_stage
|
|
37
37
|
|
|
38
38
|
ts = typed_stage
|
|
39
|
-
|
|
39
|
+
original = ts.original_abbr
|
|
40
|
+
if ts.long_abbr && original && (original.start_with?(ts.long_abbr) ||
|
|
41
|
+
ts.long_abbr.start_with?(original))
|
|
42
|
+
# The long family: the canonical long spelling, its punctuated
|
|
43
|
+
# forms ("Amd." of "Amd"), and shorter long-family spellings
|
|
44
|
+
# ("Am" — the alias of "Amd" the owner ruled renders canonical).
|
|
45
|
+
# The short family ("AMD") matches neither direction.
|
|
40
46
|
true
|
|
41
47
|
elsif ts.long_abbr && ts.original_abbr&.include?("Directives")
|
|
42
48
|
true
|
data/lib/pubid/nist/CLAUDE.md
CHANGED
|
@@ -135,3 +135,52 @@ Read this before you change `lib/pubid/nist/` or `spec/pubid/nist/`. The root
|
|
|
135
135
|
`spec/pubid/nist/bare_series_reference_spec.rb`; the corpus round froze
|
|
136
136
|
the six rows (`NBS CS`, `NBS CSM`, `NBS CIS`, `NBS CS-E`, `NIST PS`,
|
|
137
137
|
`NIST DCI`) into the testsuite with the gem twin in lockstep.
|
|
138
|
+
|
|
139
|
+
- **"FIPS PUB <number>" is the official RENDER face; the preprocessor
|
|
140
|
+
alias exists so the spellings parse (pubid#697; owner ruling
|
|
141
|
+
2026-11-12).** FIPS is government-published and "PUB" (Federal
|
|
142
|
+
Information Processing Standards PUBlication) is part of the official
|
|
143
|
+
designation, so
|
|
144
|
+
`FederalInformationProcessingStandards#fips_render_lead` renders the
|
|
145
|
+
spelled-NIST-publisher rows as "FIPS PUB <number>" with the tails
|
|
146
|
+
riding the lead; the lead reads `printed_publisher`, so the
|
|
147
|
+
series-default NIST a bare parse fills (#704) is not a printed
|
|
148
|
+
publisher and the bare face stays bare. The grammar reads "NIST FIPS
|
|
149
|
+
<number>", so `normalize_fips_pub_alias!` (head-anchored, immediately
|
|
150
|
+
after the publisher/series stage) folds every printed spelling —
|
|
151
|
+
"FIPS PUB 205" and the publisher-prefixed "NIST FIPS PUB 205"
|
|
152
|
+
(pubid#702) — onto that canonical model, and all the spellings
|
|
153
|
+
converge on one identity: hash-, URN- and MR-identical, and one
|
|
154
|
+
render. The hyphenated numbers and the revision forms the grammar
|
|
155
|
+
models ("FIPS PUB 199 Rev. 1" with its `original_prefix` facet,
|
|
156
|
+
compact "rev1") ride the prefix alias; bare "FIPS <number>"
|
|
157
|
+
(publisher_was_parsed: false), the historical NBS-prefixed rows, and
|
|
158
|
+
the MR face ("NIST.FIPS.197", an identity surface) are untouched.
|
|
159
|
+
|
|
160
|
+
- **A bare "FIPS <number>" carries the series' NIST publisher (hand-off
|
|
161
|
+
nist-bare-fips-publisher, metanorma-pdfa#114).** relaton keeps a CSRC hit
|
|
162
|
+
only when the parsed citation is `==` to the row, and every row is
|
|
163
|
+
"NIST FIPS …", so a bare "FIPS 197" (publisher nil) answered "Not found".
|
|
164
|
+
The Builder now fills `identifier.class.series_default_publisher`
|
|
165
|
+
("NIST" on `FederalInformationProcessingStandards`, nil on the base)
|
|
166
|
+
**after** it records `publisher_was_parsed: false`. The flag is
|
|
167
|
+
`equality_ignored`, so the two spellings are `==`; the short and MR faces
|
|
168
|
+
print the publisher only when it was spelled (`printed_publisher`), so the
|
|
169
|
+
bare face still renders `FIPS 197` / `FIPS.197`. The long and abbreviated
|
|
170
|
+
styles now name NIST (before, they began with a stray space). "NBS FIPS"
|
|
171
|
+
keeps its parsed NBS and is not `==` to the bare form. **NBS-era ruling**:
|
|
172
|
+
`#matches?` treats a publisher the query did not spell as a wildcard (and
|
|
173
|
+
no longer compares `publisher_was_parsed`, a spelling), so the query
|
|
174
|
+
"FIPS 46" still finds "NBS FIPS 46" — before this, a bare query matched
|
|
175
|
+
neither the NIST nor the NBS row. `===` needs the same rule
|
|
176
|
+
(`subset_skipped_attributes`), because the filled publisher is no longer
|
|
177
|
+
the nil the subset match read as "anything"; **the two primitives must
|
|
178
|
+
agree, so change them together.** `to_hash` of a bare parse gains
|
|
179
|
+
`publisher: NIST`; the 21,365-id `full/` corpus has no bare FIPS row, so
|
|
180
|
+
no fixture output moved. `from_hash` fills the default by the same rule
|
|
181
|
+
(`apply_series_default_publisher`, shared with the Builder), so a row
|
|
182
|
+
stored before this change (`publisher_was_parsed: false`, no publisher)
|
|
183
|
+
is still `==` to the parse. Not covered: `new(series:, number:)` fills
|
|
184
|
+
nothing (its flag defaults to true, so it means a spelled form anyway).
|
|
185
|
+
Locked by
|
|
186
|
+
`spec/pubid/nist/bare_fips_publisher_spec.rb`.
|
data/lib/pubid/nist/builder.rb
CHANGED
|
@@ -422,6 +422,11 @@ module Pubid
|
|
|
422
422
|
# to_hash) when one was. Keeps the common publisher-bearing id lean.
|
|
423
423
|
identifier.publisher_was_parsed = false unless identifier.publisher
|
|
424
424
|
|
|
425
|
+
# A series that implies its publisher (bare "FIPS 197" is
|
|
426
|
+
# "NIST FIPS 197") gets it now, after the flag above, so the two
|
|
427
|
+
# spellings are == while the bare face keeps rendering bare.
|
|
428
|
+
identifier.apply_series_default_publisher
|
|
429
|
+
|
|
425
430
|
# NEW: Convert revision with month+year to update component (V1 compatibility)
|
|
426
431
|
# Patterns like "NIST IR 4743rJun1992" should be rendered as "NIST IR 4743/Upd1-199206"
|
|
427
432
|
if parsed_hash[:revision_month] && parsed_hash[:revision_year]
|
|
@@ -39,6 +39,10 @@ module Pubid
|
|
|
39
39
|
id = super(normalize_deserialized_shapes(expand_compact_hash(data)),
|
|
40
40
|
options)
|
|
41
41
|
derive_subseries(id)
|
|
42
|
+
# A bare-FIPS row written before the series default existed holds
|
|
43
|
+
# `publisher_was_parsed: false` and no publisher; parse now fills
|
|
44
|
+
# it, so from_hash must too, or the row is no longer == the parse.
|
|
45
|
+
id.apply_series_default_publisher if id.is_a?(Identifier)
|
|
42
46
|
id
|
|
43
47
|
end
|
|
44
48
|
|
|
@@ -291,6 +295,29 @@ module Pubid
|
|
|
291
295
|
super + %i[update update_component]
|
|
292
296
|
end
|
|
293
297
|
|
|
298
|
+
# The publisher a series implies when the input spells none. The
|
|
299
|
+
# Builder fills it after it records `publisher_was_parsed: false`, so
|
|
300
|
+
# the bare face still renders without the prefix. nil: no default.
|
|
301
|
+
def self.series_default_publisher
|
|
302
|
+
nil
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Fill the series default publisher on an id whose input spelled
|
|
306
|
+
# none. Shared by the Builder and from_hash, so both construction
|
|
307
|
+
# paths agree.
|
|
308
|
+
def apply_series_default_publisher
|
|
309
|
+
return if publisher || publisher_was_parsed
|
|
310
|
+
|
|
311
|
+
self.publisher = self.class.series_default_publisher
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# `===` (subset match) follows #matches?: a publisher the reference
|
|
315
|
+
# did not spell is a wildcard, so bare "FIPS 46" finds "NBS FIPS 46".
|
|
316
|
+
def subset_skipped_attributes
|
|
317
|
+
skipped = super
|
|
318
|
+
publisher_was_parsed ? skipped : skipped | [:publisher]
|
|
319
|
+
end
|
|
320
|
+
|
|
294
321
|
# Wildcard / partial-identifier match. Treats +self+ as a QUERY pattern
|
|
295
322
|
# and +candidate+ as a concrete document: every ID part SET on the query
|
|
296
323
|
# must equal the candidate's, while parts left unset (nil/empty) are
|
|
@@ -301,11 +328,16 @@ module Pubid
|
|
|
301
328
|
# Asymmetric (unlike ==): "NBS CIRC 25".matches?("NBS CIRC 25sup1924")
|
|
302
329
|
# is true, but not the reverse. The candidate must be the same class or
|
|
303
330
|
# a subclass so series-level identity still holds.
|
|
331
|
+
#
|
|
332
|
+
# `publisher_was_parsed` records only how the query was spelled, so it
|
|
333
|
+
# is never a criterion; and a publisher the query did not spell (the
|
|
334
|
+
# series default a bare "FIPS 46" fills in) is a wildcard, so the bare
|
|
335
|
+
# query still finds the NBS-era "NBS FIPS 46".
|
|
304
336
|
def matches?(candidate)
|
|
305
337
|
return false unless candidate.is_a?(self.class)
|
|
306
338
|
|
|
307
339
|
self.class.attributes.each_key.all? do |name|
|
|
308
|
-
next true
|
|
340
|
+
next true unless match_criterion?(name)
|
|
309
341
|
|
|
310
342
|
query_val = public_send(name)
|
|
311
343
|
next true if query_val.nil?
|
|
@@ -316,6 +348,15 @@ module Pubid
|
|
|
316
348
|
end
|
|
317
349
|
end
|
|
318
350
|
|
|
351
|
+
# Whether #matches? compares attribute +name+ at all.
|
|
352
|
+
def match_criterion?(name)
|
|
353
|
+
return false if EQUALITY_IGNORED_ATTRS.include?(name)
|
|
354
|
+
return false if name == :publisher_was_parsed
|
|
355
|
+
|
|
356
|
+
name != :publisher || publisher_was_parsed
|
|
357
|
+
end
|
|
358
|
+
private :match_criterion?
|
|
359
|
+
|
|
319
360
|
# NIST inherits Pubid::Identifier#exclude directly: it now rebuilds via
|
|
320
361
|
# `self.class.new(**attrs)`, so NIST's keyword-only initialize
|
|
321
362
|
# (initialize(**attributes)) works without a local override, and the base
|
|
@@ -6,7 +6,9 @@ module Pubid
|
|
|
6
6
|
# NIST Federal Information Processing Standards (FIPS)
|
|
7
7
|
# Examples:
|
|
8
8
|
# - "FIPS 140-3" (no NIST prefix)
|
|
9
|
-
# - "NIST FIPS 140-3" (also accepted
|
|
9
|
+
# - "NIST FIPS 140-3" / "FIPS PUB 140-3" (also accepted; both parse
|
|
10
|
+
# to the same identity, and the NIST-published face renders as
|
|
11
|
+
# "FIPS PUB 140-3")
|
|
10
12
|
class FederalInformationProcessingStandards < Identifier
|
|
11
13
|
TYPED_STAGES = [
|
|
12
14
|
Pubid::Components::TypedStage.new(
|
|
@@ -21,6 +23,13 @@ module Pubid
|
|
|
21
23
|
TYPED_STAGES
|
|
22
24
|
end
|
|
23
25
|
|
|
26
|
+
# Bare "FIPS 197" names the NIST document (NIST/CSRC list every
|
|
27
|
+
# FIPS, NBS-era ones included, as "NIST FIPS"). A parsed "NBS"
|
|
28
|
+
# stays NBS.
|
|
29
|
+
def series_default_publisher
|
|
30
|
+
"NIST"
|
|
31
|
+
end
|
|
32
|
+
|
|
24
33
|
def type
|
|
25
34
|
{ key: :fips,
|
|
26
35
|
web: :federal_information_processing_standards, title: "Federal Information Processing Standards",
|
|
@@ -41,8 +50,10 @@ module Pubid
|
|
|
41
50
|
def to_mr_style
|
|
42
51
|
# "NIST.FIPS.197" keeps a parsed publisher (the DOI suffix NIST
|
|
43
52
|
# publishes); "FIPS.46e1977" has none and invents none.
|
|
53
|
+
# A bare "FIPS 197" carries the series default publisher NIST but
|
|
54
|
+
# did not spell it, so it prints no prefix either.
|
|
44
55
|
# Note: edition is appended directly to number without dot for FIPS
|
|
45
|
-
result = [
|
|
56
|
+
result = [printed_publisher, series_code].compact.join(".")
|
|
46
57
|
result += ".#{number}" if number
|
|
47
58
|
|
|
48
59
|
# FIPS uses dash notation for parts; render here and skip the generic
|
|
@@ -58,10 +69,30 @@ module Pubid
|
|
|
58
69
|
|
|
59
70
|
public
|
|
60
71
|
|
|
72
|
+
# The short-face lead: the official printed "FIPS PUB" for a spelled
|
|
73
|
+
# NIST publisher (owner ruling 2026-11-12), the spelled historical
|
|
74
|
+
# publisher for NBS rows, and the bare series code when the input
|
|
75
|
+
# spelled no publisher — the series-default NIST a bare parse fills
|
|
76
|
+
# (#704) is not a printed publisher, so the bare face stays bare.
|
|
77
|
+
def fips_render_lead
|
|
78
|
+
lead_publisher = printed_publisher
|
|
79
|
+
return "FIPS PUB" if lead_publisher == "NIST"
|
|
80
|
+
|
|
81
|
+
spelled = lead_publisher && !lead_publisher.empty?
|
|
82
|
+
return "#{lead_publisher} #{series_code}" if spelled
|
|
83
|
+
|
|
84
|
+
series_code.to_s
|
|
85
|
+
end
|
|
86
|
+
|
|
61
87
|
def to_short_style
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
|
|
88
|
+
# The official published face is "FIPS PUB <number>": FIPS is
|
|
89
|
+
# government-published, and "PUB" (Federal Information Processing
|
|
90
|
+
# Standards PUBlication) is part of the official designation
|
|
91
|
+
# (owner ruling 2026-11-12). The tail (volume/part/edition/
|
|
92
|
+
# supplement/update/draft) rides the lead unchanged, and the MR
|
|
93
|
+
# face ("NIST.FIPS.197") is an identity surface that does not
|
|
94
|
+
# move.
|
|
95
|
+
result = fips_render_lead
|
|
65
96
|
|
|
66
97
|
if number
|
|
67
98
|
# Convert dash-year patterns to edition format for FIPS
|
|
@@ -90,6 +121,14 @@ module Pubid
|
|
|
90
121
|
|
|
91
122
|
result
|
|
92
123
|
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
# The publisher the short and MR faces print: only one the input
|
|
128
|
+
# spelled, never the series default.
|
|
129
|
+
def printed_publisher
|
|
130
|
+
publisher if publisher_was_parsed
|
|
131
|
+
end
|
|
93
132
|
end
|
|
94
133
|
end
|
|
95
134
|
end
|
|
@@ -68,6 +68,7 @@ module Pubid
|
|
|
68
68
|
end
|
|
69
69
|
normalize_spurious_u_suffix!
|
|
70
70
|
normalize_publisher_and_series!
|
|
71
|
+
normalize_fips_pub_alias!
|
|
71
72
|
normalize_lcirc_supplement_contexts!
|
|
72
73
|
normalize_revision_spacing!
|
|
73
74
|
normalize_letter_suffix_casing!
|
|
@@ -124,6 +125,23 @@ module Pubid
|
|
|
124
125
|
@cleaned = @cleaned.gsub(/\bLC\b(?!IRC)/, "LCIRC")
|
|
125
126
|
end
|
|
126
127
|
|
|
128
|
+
# The FIPS series' official printed face: FIPS is government-published
|
|
129
|
+
# and "PUB" (Federal Information Processing Standards PUBlication) is
|
|
130
|
+
# part of the official designation, so the RENDERER emits
|
|
131
|
+
# "FIPS PUB <number>" directly (owner ruling 2026-11-12). The grammar
|
|
132
|
+
# itself reads the publisher-bearing spelling only through this alias
|
|
133
|
+
# (pubid#697): it folds the printed spellings — "FIPS PUB 205" and the
|
|
134
|
+
# publisher-prefixed "NIST FIPS PUB 205" (pubid#702/#703) — onto the
|
|
135
|
+
# one model "NIST FIPS <number>" the grammar already builds (same
|
|
136
|
+
# to_hash, same URN, same MR string; revision spellings like
|
|
137
|
+
# "FIPS PUB 199 Rev. 1" included), while the renderer owns the printed
|
|
138
|
+
# face the fold converges to. Anchored at the head and run before
|
|
139
|
+
# every other stage so later digit/marker stages see the canonical
|
|
140
|
+
# spelling; "FIPS" alone (publisher_was_parsed: false) is untouched.
|
|
141
|
+
def normalize_fips_pub_alias!
|
|
142
|
+
@cleaned = @cleaned.sub(/\A(?:NIST )?FIPS PUB\b/i, "NIST FIPS")
|
|
143
|
+
end
|
|
144
|
+
|
|
127
145
|
# LCIRC series: combine with NBS prefix when a supplement marker
|
|
128
146
|
# follows, and convert MR-format dots to spaces so the grammar's
|
|
129
147
|
# circ_supplement_identifier rule can match.
|
|
@@ -14,7 +14,7 @@ module Pubid
|
|
|
14
14
|
# Examples:
|
|
15
15
|
# - urn:nist:sp:800-53.supp → NIST SP 800-53
|
|
16
16
|
# - urn:nist:sp:800-53.r5.supp → NIST SP 800-53r5
|
|
17
|
-
# - urn:nist:fips:199.supp →
|
|
17
|
+
# - urn:nist:fips:199.supp → FIPS PUB 199
|
|
18
18
|
class UrnParser < Pubid::UrnParser::Base
|
|
19
19
|
TYPE_UPCASE = {
|
|
20
20
|
"sp" => "SP",
|
data/lib/pubid/plateau/CLAUDE.md
CHANGED
|
@@ -11,3 +11,38 @@ Integer, so its core is `:int` (`core: [["number", :int]]`): `#00` is the core
|
|
|
11
11
|
`0`, and `00` in a URN is not canonical. The Annex type has no `number`; it
|
|
12
12
|
has no core, its document comes first along `base`, and its `letter` is an
|
|
13
13
|
atom. No input makes an Annex today, so the spec builds one by hand.
|
|
14
|
+
|
|
15
|
+
## Handbook volume label (本編 / 本編+付録)
|
|
16
|
+
|
|
17
|
+
MLIT publishes Handbook #00 edition 5.0 twice on one date: `第5.0版 本編`
|
|
18
|
+
(main volume) and `第5.0版 本編+付録` (main volume and appendix, one PDF);
|
|
19
|
+
edition 6.0 only as `本編+付録`. `Identifiers::Handbook#volume` keeps that
|
|
20
|
+
label as Japanese text, as `edition` keeps `第X.Y版`. The grammar accepts it
|
|
21
|
+
only after an edition, separated by an ASCII or a full-width (U+3000) space,
|
|
22
|
+
with `+` or ASCII `+`; the builder normalizes `+` to `+`, and `to_s` writes
|
|
23
|
+
one ASCII space before it. The renderer prints the volume only after an
|
|
24
|
+
edition, so `exclude(:edition)` still renders a string that parses.
|
|
25
|
+
`volume` is in `==` and is `subset_strict`, so `#00 第5.0版` matches neither
|
|
26
|
+
labelled publication. A nil volume leaves no key in `to_hash`, so the
|
|
27
|
+
existing index-v2 rows keep their hash. The pubid-scheme URN carries it as
|
|
28
|
+
the `vol-` atom; `to_urn` is unchanged (it has no edition either). Locked by
|
|
29
|
+
`spec/pubid/plateau/handbook_volume_spec.rb`.
|
|
30
|
+
|
|
31
|
+
Known gaps, not caused by this change: a Technical Report drops an edition
|
|
32
|
+
and a volume without a diagnostic (the `handbook` rule matches any
|
|
33
|
+
`doc_type`), and `to_mr_string` raises for every Handbook with an edition
|
|
34
|
+
(the base `mr_edition` calls `edition.number` on a String).
|
|
35
|
+
|
|
36
|
+
## The grammar is baked here ahead of pubid-grammar
|
|
37
|
+
|
|
38
|
+
`data/parg/plateau.json` was compiled here from its own embedded `source`
|
|
39
|
+
plus the volume rule, at parsanol **1.3.114** (the version of the earlier
|
|
40
|
+
bake; force it with `gem "parsanol", "1.3.114"`, as
|
|
41
|
+
`pubid-grammar/scripts/bake-one.rb` does, because the binstub resolves the
|
|
42
|
+
latest installed gem). Until pubid-grammar takes hand-off
|
|
43
|
+
`pubid__pubid-grammar__plateau-handbook-volume`, its `grammars/plateau.parg`
|
|
44
|
+
is the OLD grammar. To change the grammar, extract the source with
|
|
45
|
+
`jq -j .source data/parg/plateau.json` (`-j`: the source has no trailing
|
|
46
|
+
newline, and one extra byte changes the checksum), edit, re-bake, then run
|
|
47
|
+
`bundle exec parsanol parg test data/parg/plateau.json`. Read the `.parg`
|
|
48
|
+
file as UTF-8: under a US-ASCII locale the parsanol lexer raises on `第`.
|
|
@@ -34,6 +34,10 @@ module Pubid
|
|
|
34
34
|
if parsed[:edition] && klass == Identifiers::Handbook
|
|
35
35
|
params[:edition] = parsed[:edition].to_s
|
|
36
36
|
end
|
|
37
|
+
# The grammar accepts an ASCII "+"; MLIT prints the full-width "+".
|
|
38
|
+
if parsed[:volume] && klass == Identifiers::Handbook
|
|
39
|
+
params[:volume] = parsed[:volume].to_s.tr("+", "+")
|
|
40
|
+
end
|
|
37
41
|
|
|
38
42
|
klass.new(**params)
|
|
39
43
|
end
|