pubid 2.0.21 → 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.
@@ -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)
@@ -112,7 +112,7 @@ module Pubid
112
112
  ),
113
113
  ::Pubid::Components::TypedStage.new(
114
114
  code: :published,
115
- abbr: ["Amd", "AMD", "Amd."],
115
+ abbr: ["Amd", "AMD", "Amd.", "Am"],
116
116
  short_abbr: "AMD",
117
117
  long_abbr: "Amd",
118
118
  type_code: :amd,
@@ -36,7 +36,13 @@ module Pubid
36
36
  return false unless typed_stage
37
37
 
38
38
  ts = typed_stage
39
- if ts.long_abbr && ts.original_abbr&.start_with?(ts.long_abbr)
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
@@ -136,13 +136,51 @@ 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 a preprocessor alias, not a grammar face
140
- (pubid#697).** NIST prints the series as "FIPS PUB <number>" but the
141
- grammar reads "NIST FIPS <number>", so `normalize_fips_pub_alias!`
142
- (head-anchored, immediately after the publisher/series stage) rewrites
143
- the printed prefix and the parse lands on exactly the canonical model —
144
- hash-, render-, URN- and MR-identical to "NIST FIPS <number>". The
145
- hyphenated numbers and the revision forms the grammar models
146
- ("FIPS PUB 199 Rev. 1" with its `original_prefix` facet, compact
147
- "rev1") ride the prefix alias; bare "FIPS <number>"
148
- (publisher_was_parsed: false) and NBS FIPS are untouched.
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`.
@@ -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 if EQUALITY_IGNORED_ATTRS.include?(name)
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, normalizes to FIPS 140-3)
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 = [publisher, series_code].compact.join(".")
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
- # FIPS format: "FIPS 14e1971" or "NIST FIPS 140-3" (preserve publisher if set)
63
- result = publisher ? "#{publisher} " : ""
64
- result += series_code
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
@@ -125,19 +125,21 @@ module Pubid
125
125
  @cleaned = @cleaned.gsub(/\bLC\b(?!IRC)/, "LCIRC")
126
126
  end
127
127
 
128
- # The FIPS series' own printed face: NIST prints the series as
129
- # "FIPS PUB <number>" (document cover and catalogue style), so the
130
- # grammar accepts the publisher-prefixed canonical form only through
131
- # this alias (pubid#697). Rewrites the prefix to "NIST FIPS", the
132
- # spelling the grammar already reads, and the printed-form parse
133
- # therefore lands on exactly the model "NIST FIPS <number>" builds
134
- # (same to_hash, same render, revision spellings like
135
- # "FIPS PUB 199 Rev. 1" included). Anchored at the head and run
136
- # before every other stage so later digit/marker stages see the
137
- # canonical face; "FIPS" alone (publisher_was_parsed: false) is
138
- # untouched.
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(/\AFIPS PUB\b/i, "NIST FIPS")
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
@@ -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 → NIST FIPS 199
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pubid
4
- VERSION = "2.0.21"
4
+ VERSION = "2.0.22"
5
5
  end
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.21
4
+ version: 2.0.22
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 00:00:00.000000000 Z
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
@@ -142,6 +144,7 @@ files:
142
144
  - data/parg/tables/organizations.yaml
143
145
  - data/parg/tables/tc_types.yaml
144
146
  - data/parg/tables/typed_stages.yaml
147
+ - data/parg/tables/typed_stages_ballots.yaml
145
148
  - data/parg/tables/typed_stages_supplements.yaml
146
149
  - data/parg/tables/wg_types.yaml
147
150
  - data/parg/tgpp.json