pubid 2.0.21 → 2.0.23
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/iso/historic-schemes.md +368 -0
- data/data/parg/doi_pfs.json +1 -0
- data/data/parg/iec.json +1 -1
- data/data/parg/iso.json +1 -1
- data/data/parg/iso.parg +142 -2
- data/data/parg/nist.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/doi_pfs/CLAUDE.md +72 -0
- data/lib/pubid/doi_pfs/builder.rb +18 -0
- data/lib/pubid/doi_pfs/identifier.rb +36 -0
- data/lib/pubid/doi_pfs/identifiers/resource.rb +31 -0
- data/lib/pubid/doi_pfs/identifiers.rb +7 -0
- data/lib/pubid/doi_pfs/parser.rb +10 -0
- data/lib/pubid/doi_pfs/pubid_urn_schema.rb +15 -0
- data/lib/pubid/doi_pfs/renderer.rb +10 -0
- data/lib/pubid/doi_pfs/urn_generator.rb +12 -0
- data/lib/pubid/doi_pfs.rb +68 -0
- data/lib/pubid/iec/CLAUDE.md +5 -1
- data/lib/pubid/iec/builder.rb +47 -0
- data/lib/pubid/iec/identifiers/amendment.rb +34 -0
- data/lib/pubid/iec/identifiers/corrigendum.rb +6 -1
- data/lib/pubid/iec/supplement_identifier.rb +55 -25
- data/lib/pubid/iho/CLAUDE.md +2 -1
- data/lib/pubid/iho/pubid_urn_schema.rb +2 -1
- 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 +84 -10
- data/lib/pubid/nist/builder.rb +15 -0
- data/lib/pubid/nist/identifiers/base.rb +52 -1
- data/lib/pubid/nist/identifiers/federal_information_processing_standards.rb +44 -5
- data/lib/pubid/nist/identifiers/special_publication.rb +15 -0
- data/lib/pubid/nist/preprocessor.rb +14 -12
- data/lib/pubid/nist/router.rb +11 -0
- data/lib/pubid/nist/urn_parser.rb +1 -1
- data/lib/pubid/version.rb +1 -1
- data/lib/pubid.rb +4 -0
- metadata +16 -2
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
|
@@ -136,13 +136,87 @@ Read this before you change `lib/pubid/nist/` or `spec/pubid/nist/`. The root
|
|
|
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
138
|
|
|
139
|
-
- **"FIPS PUB <number>" is
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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`.
|
|
187
|
+
|
|
188
|
+
- **The default-series plain-number branch (pubid#711).** A citation
|
|
189
|
+
shaped like the docnumber — "800-90B", the docid-derived citation face
|
|
190
|
+
of NIST.SP.800-90B — and its publisher-led spelling "NIST 800-90B"
|
|
191
|
+
carry NO series token, and the grammar's series alternatives never
|
|
192
|
+
reached them: every branch demanded a series token (or, after a bare
|
|
193
|
+
publisher, only the dated ISO-date digits). The grammar (nist.parg,
|
|
194
|
+
fleet compiler 1.3.126) gained two trailing `identifier` alternatives
|
|
195
|
+
that ride the SAME report-number tail as the series-bearing branches —
|
|
196
|
+
`(publisher) (space) (report_number) …` and bare `(report_number) …` —
|
|
197
|
+
placed LAST so PEG ordering leaves every previously accepted form on
|
|
198
|
+
its original alternative (they only catch inputs that previously
|
|
199
|
+
failed outright). The identity defaults onto the SP series: the Router
|
|
200
|
+
dispatches `SpecialPublication` when the parse carries no series and
|
|
201
|
+
no dated date, and the Builder fills `series` from the new
|
|
202
|
+
`Identifier.default_series` class method (SP is the default — the
|
|
203
|
+
docid of the real document is NIST.SP.800-90B). The publisher rides
|
|
204
|
+
the #704 machinery: `SpecialPublication.series_default_publisher` is
|
|
205
|
+
now "NIST", so the bare parse fills the publisher after
|
|
206
|
+
`publisher_was_parsed: false` and is `==` to the spelled form (URN-,
|
|
207
|
+
MR- and hash-equal but for that equality-ignored flag; the MR face is
|
|
208
|
+
the exact "NIST.SP.800-90B" DOI surface relaton-data-nist stores).
|
|
209
|
+
The SHORT face keeps the bare spelling (publisher suppressed);
|
|
210
|
+
the MR face always carries the publisher (`to_mr_style` prints
|
|
211
|
+
`(publisher || "NIST")`), so the bare parse lands on the DOI-style
|
|
212
|
+
face directly. The dated alternative keeps priority
|
|
213
|
+
("NIST 2023-04-15 001" stays `DatedDocument`), and the routing probe
|
|
214
|
+
("zzqx-nonsense-9714") still fails, so NIST is not disqualified as a
|
|
215
|
+
`Pubid.parse` fallback. `to_hash` of the two bare-series probes
|
|
216
|
+
("SP 123", "SP 800-90B") gained `publisher: NIST` — the #704 ruling
|
|
217
|
+
generalized to the SP series; no corpus row moved. Locked by
|
|
218
|
+
`spec/pubid/nist/default_series_plain_number_spec.rb`; the corpus
|
|
219
|
+
round froze the four faces (the two publisher-led spellings as
|
|
220
|
+
non-normalized aliases of the canonical cases, the two bare ones as
|
|
221
|
+
canonical `publisher_was_parsed: false` cases) with the gem twin in
|
|
222
|
+
lockstep.
|
data/lib/pubid/nist/builder.rb
CHANGED
|
@@ -99,6 +99,16 @@ module Pubid
|
|
|
99
99
|
# Locate the appropriate identifier class via Router
|
|
100
100
|
identifier = @router.locate_identifier_klass(parsed_hash).new
|
|
101
101
|
|
|
102
|
+
# The default-series plain-number branch (pubid#711): the grammar's
|
|
103
|
+
# trailing alternatives carry no series token ("800-90B" /
|
|
104
|
+
# "NIST 800-90B"), so the identity gains the class's default series
|
|
105
|
+
# and parses == to the spelled "NIST SP 800-90B" form. The bare
|
|
106
|
+
# publisher rides the #704 rule below (apply_series_default_publisher).
|
|
107
|
+
if parsed_hash[:series].nil? &&
|
|
108
|
+
(default = identifier.class.default_series)
|
|
109
|
+
identifier.series = Components::Code.new(value: default)
|
|
110
|
+
end
|
|
111
|
+
|
|
102
112
|
# Resolve the series policy once — every series-specific decision
|
|
103
113
|
# flows through this object instead of branching on the parsed shape.
|
|
104
114
|
series = Series.for(parsed_hash)
|
|
@@ -422,6 +432,11 @@ module Pubid
|
|
|
422
432
|
# to_hash) when one was. Keeps the common publisher-bearing id lean.
|
|
423
433
|
identifier.publisher_was_parsed = false unless identifier.publisher
|
|
424
434
|
|
|
435
|
+
# A series that implies its publisher (bare "FIPS 197" is
|
|
436
|
+
# "NIST FIPS 197") gets it now, after the flag above, so the two
|
|
437
|
+
# spellings are == while the bare face keeps rendering bare.
|
|
438
|
+
identifier.apply_series_default_publisher
|
|
439
|
+
|
|
425
440
|
# NEW: Convert revision with month+year to update component (V1 compatibility)
|
|
426
441
|
# Patterns like "NIST IR 4743rJun1992" should be rendered as "NIST IR 4743/Upd1-199206"
|
|
427
442
|
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,39 @@ 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
|
+
# The series an input implies when it spells none — the grammar's
|
|
306
|
+
# default-series plain-number branch (pubid#711: "800-90B" /
|
|
307
|
+
# "NIST 800-90B" carry no series token). The Router dispatches the
|
|
308
|
+
# class from this default and the Builder fills the attribute, so a
|
|
309
|
+
# docnumber-shaped citation parses onto the same identity as the
|
|
310
|
+
# spelled form. nil: no default.
|
|
311
|
+
def self.default_series
|
|
312
|
+
nil
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Fill the series default publisher on an id whose input spelled
|
|
316
|
+
# none. Shared by the Builder and from_hash, so both construction
|
|
317
|
+
# paths agree.
|
|
318
|
+
def apply_series_default_publisher
|
|
319
|
+
return if publisher || publisher_was_parsed
|
|
320
|
+
|
|
321
|
+
self.publisher = self.class.series_default_publisher
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# `===` (subset match) follows #matches?: a publisher the reference
|
|
325
|
+
# did not spell is a wildcard, so bare "FIPS 46" finds "NBS FIPS 46".
|
|
326
|
+
def subset_skipped_attributes
|
|
327
|
+
skipped = super
|
|
328
|
+
publisher_was_parsed ? skipped : skipped | [:publisher]
|
|
329
|
+
end
|
|
330
|
+
|
|
294
331
|
# Wildcard / partial-identifier match. Treats +self+ as a QUERY pattern
|
|
295
332
|
# and +candidate+ as a concrete document: every ID part SET on the query
|
|
296
333
|
# must equal the candidate's, while parts left unset (nil/empty) are
|
|
@@ -301,11 +338,16 @@ module Pubid
|
|
|
301
338
|
# Asymmetric (unlike ==): "NBS CIRC 25".matches?("NBS CIRC 25sup1924")
|
|
302
339
|
# is true, but not the reverse. The candidate must be the same class or
|
|
303
340
|
# a subclass so series-level identity still holds.
|
|
341
|
+
#
|
|
342
|
+
# `publisher_was_parsed` records only how the query was spelled, so it
|
|
343
|
+
# is never a criterion; and a publisher the query did not spell (the
|
|
344
|
+
# series default a bare "FIPS 46" fills in) is a wildcard, so the bare
|
|
345
|
+
# query still finds the NBS-era "NBS FIPS 46".
|
|
304
346
|
def matches?(candidate)
|
|
305
347
|
return false unless candidate.is_a?(self.class)
|
|
306
348
|
|
|
307
349
|
self.class.attributes.each_key.all? do |name|
|
|
308
|
-
next true
|
|
350
|
+
next true unless match_criterion?(name)
|
|
309
351
|
|
|
310
352
|
query_val = public_send(name)
|
|
311
353
|
next true if query_val.nil?
|
|
@@ -316,6 +358,15 @@ module Pubid
|
|
|
316
358
|
end
|
|
317
359
|
end
|
|
318
360
|
|
|
361
|
+
# Whether #matches? compares attribute +name+ at all.
|
|
362
|
+
def match_criterion?(name)
|
|
363
|
+
return false if EQUALITY_IGNORED_ATTRS.include?(name)
|
|
364
|
+
return false if name == :publisher_was_parsed
|
|
365
|
+
|
|
366
|
+
name != :publisher || publisher_was_parsed
|
|
367
|
+
end
|
|
368
|
+
private :match_criterion?
|
|
369
|
+
|
|
319
370
|
# NIST inherits Pubid::Identifier#exclude directly: it now rebuilds via
|
|
320
371
|
# `self.class.new(**attrs)`, so NIST's keyword-only initialize
|
|
321
372
|
# (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
|
|
@@ -35,6 +35,21 @@ module Pubid
|
|
|
35
35
|
{ key: :sp,
|
|
36
36
|
web: :special_publication, title: "NIST Special Publication", short: "SP" }
|
|
37
37
|
end
|
|
38
|
+
|
|
39
|
+
# The default-series plain-number branch (pubid#711): a citation
|
|
40
|
+
# shaped like the docnumber ("800-90B", publisher-led
|
|
41
|
+
# "NIST 800-90B") names an SP document — the docid-derived
|
|
42
|
+
# citation of NIST.SP.800-90B is exactly "800-90B".
|
|
43
|
+
def default_series
|
|
44
|
+
"SP"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# A bare plain-number parse carries the series' NIST publisher
|
|
48
|
+
# (the #704 shape), so it is == to the spelled
|
|
49
|
+
# "NIST SP 800-90B" form a lookup compares against.
|
|
50
|
+
def series_default_publisher
|
|
51
|
+
"NIST"
|
|
52
|
+
end
|
|
38
53
|
end
|
|
39
54
|
|
|
40
55
|
def series_code
|
|
@@ -125,19 +125,21 @@ module Pubid
|
|
|
125
125
|
@cleaned = @cleaned.gsub(/\bLC\b(?!IRC)/, "LCIRC")
|
|
126
126
|
end
|
|
127
127
|
|
|
128
|
-
# The FIPS series'
|
|
129
|
-
# "
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
#
|
|
138
|
-
#
|
|
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.
|
|
139
141
|
def normalize_fips_pub_alias!
|
|
140
|
-
@cleaned = @cleaned.sub(/\
|
|
142
|
+
@cleaned = @cleaned.sub(/\A(?:NIST )?FIPS PUB\b/i, "NIST FIPS")
|
|
141
143
|
end
|
|
142
144
|
|
|
143
145
|
# LCIRC series: combine with NBS prefix when a supplement marker
|
data/lib/pubid/nist/router.rb
CHANGED
|
@@ -46,6 +46,17 @@ module Pubid
|
|
|
46
46
|
|
|
47
47
|
series = parsed_hash[:series]&.to_s
|
|
48
48
|
|
|
49
|
+
# The default-series plain-number branch (pubid#711): the grammar's
|
|
50
|
+
# trailing alternatives accept a bare report number ("800-90B") and
|
|
51
|
+
# its publisher-led spelling ("NIST 800-90B") with NO series token.
|
|
52
|
+
# The SP series is the default: the docid-derived citation of
|
|
53
|
+
# NIST.SP.800-90B is exactly the bare docnumber "800-90B", and the
|
|
54
|
+
# Builder fills the same SP series the spelled form carries, so the
|
|
55
|
+
# two spellings are one identity to a lookup. No other branch can
|
|
56
|
+
# arrive here series-less (dated shapes returned above; every
|
|
57
|
+
# series-bearing branch captures :series).
|
|
58
|
+
return Identifiers::SpecialPublication if series.nil?
|
|
59
|
+
|
|
49
60
|
# Handle compound series that include publisher (both space and dot separated)
|
|
50
61
|
if series&.start_with?("NBS ")
|
|
51
62
|
simple_series = series.sub("NBS ", "")
|
|
@@ -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/version.rb
CHANGED
data/lib/pubid.rb
CHANGED
|
@@ -224,6 +224,10 @@ module Pubid
|
|
|
224
224
|
autoload :Ashrae, "pubid/ashrae"
|
|
225
225
|
autoload :Asme, "pubid/asme"
|
|
226
226
|
autoload :Doi, "pubid/doi"
|
|
227
|
+
# The PFS-compiled dev alias of doi (pubid/pubid#706): registered
|
|
228
|
+
# beside the coded flavor, never routed by Pubid.parse (registry keys
|
|
229
|
+
# sort "doi" first).
|
|
230
|
+
autoload :DoiPfs, "pubid/doi_pfs"
|
|
227
231
|
autoload :Easc, "pubid/easc"
|
|
228
232
|
autoload :Astm, "pubid/astm"
|
|
229
233
|
autoload :Bipm, "pubid/bipm"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pubid
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.
|
|
4
|
+
version: 2.0.23
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-11-
|
|
11
|
+
date: 2026-11-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: lutaml-model
|
|
@@ -68,10 +68,12 @@ files:
|
|
|
68
68
|
- conformance/pending.yaml
|
|
69
69
|
- data/bipm/update_codes.yaml
|
|
70
70
|
- data/ccsds/update_codes.yaml
|
|
71
|
+
- data/iec/legacy-notation.md
|
|
71
72
|
- data/iec/stage-codes.md
|
|
72
73
|
- data/iec/update_codes.yaml
|
|
73
74
|
- data/ieee/update_codes.yaml
|
|
74
75
|
- data/iho/s4-reserved-specs.yaml
|
|
76
|
+
- data/iso/historic-schemes.md
|
|
75
77
|
- data/iso/update_codes.yaml
|
|
76
78
|
- data/nist/series.yaml
|
|
77
79
|
- data/nist/stages.yaml
|
|
@@ -95,6 +97,7 @@ files:
|
|
|
95
97
|
- data/parg/cie.json
|
|
96
98
|
- data/parg/csa.json
|
|
97
99
|
- data/parg/doi.json
|
|
100
|
+
- data/parg/doi_pfs.json
|
|
98
101
|
- data/parg/easc.json
|
|
99
102
|
- data/parg/ecma.json
|
|
100
103
|
- data/parg/etsi.json
|
|
@@ -142,6 +145,7 @@ files:
|
|
|
142
145
|
- data/parg/tables/organizations.yaml
|
|
143
146
|
- data/parg/tables/tc_types.yaml
|
|
144
147
|
- data/parg/tables/typed_stages.yaml
|
|
148
|
+
- data/parg/tables/typed_stages_ballots.yaml
|
|
145
149
|
- data/parg/tables/typed_stages_supplements.yaml
|
|
146
150
|
- data/parg/tables/wg_types.yaml
|
|
147
151
|
- data/parg/tgpp.json
|
|
@@ -467,6 +471,16 @@ files:
|
|
|
467
471
|
- lib/pubid/doi/parser.rb
|
|
468
472
|
- lib/pubid/doi/pubid_urn_schema.rb
|
|
469
473
|
- lib/pubid/doi/renderer.rb
|
|
474
|
+
- lib/pubid/doi_pfs.rb
|
|
475
|
+
- lib/pubid/doi_pfs/CLAUDE.md
|
|
476
|
+
- lib/pubid/doi_pfs/builder.rb
|
|
477
|
+
- lib/pubid/doi_pfs/identifier.rb
|
|
478
|
+
- lib/pubid/doi_pfs/identifiers.rb
|
|
479
|
+
- lib/pubid/doi_pfs/identifiers/resource.rb
|
|
480
|
+
- lib/pubid/doi_pfs/parser.rb
|
|
481
|
+
- lib/pubid/doi_pfs/pubid_urn_schema.rb
|
|
482
|
+
- lib/pubid/doi_pfs/renderer.rb
|
|
483
|
+
- lib/pubid/doi_pfs/urn_generator.rb
|
|
470
484
|
- lib/pubid/easc.rb
|
|
471
485
|
- lib/pubid/easc/builder.rb
|
|
472
486
|
- lib/pubid/easc/identifier.rb
|