pubid 2.0.0.pre.alpha.12 → 2.0.0.pre.alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.adoc +43 -1
- data/data/ieee/update_codes.yaml +17 -4
- data/data/nist/update_codes.yaml +7 -3
- data/lib/pubid/adobe/builder.rb +2 -0
- data/lib/pubid/all_parts.rb +201 -0
- data/lib/pubid/all_parts_identifier.rb +19 -0
- data/lib/pubid/amca/CLAUDE.md +47 -0
- data/lib/pubid/amca/builder.rb +3 -5
- data/lib/pubid/amca/identifiers/base.rb +10 -0
- data/lib/pubid/amca/identifiers/publication.rb +13 -0
- data/lib/pubid/amca/parser.rb +2 -1
- data/lib/pubid/amca/renderer.rb +22 -33
- data/lib/pubid/amca/urn_generator.rb +21 -2
- data/lib/pubid/amca/urn_parser.rb +36 -10
- data/lib/pubid/ansi/builder.rb +6 -0
- data/lib/pubid/api/CLAUDE.md +23 -0
- data/lib/pubid/api/builder.rb +2 -0
- data/lib/pubid/ashrae/CLAUDE.md +13 -0
- data/lib/pubid/ashrae/builder.rb +58 -14
- data/lib/pubid/ashrae/identifiers/errata.rb +14 -2
- data/lib/pubid/ashrae/identifiers/interpretation.rb +2 -10
- data/lib/pubid/ashrae/parser.rb +62 -28
- data/lib/pubid/ashrae/renderer.rb +32 -1
- data/lib/pubid/ashrae/urn_generator.rb +32 -9
- data/lib/pubid/asme/CLAUDE.md +25 -0
- data/lib/pubid/asme/builder.rb +16 -9
- data/lib/pubid/asme/components/code.rb +2 -0
- data/lib/pubid/asme/identifiers/standard.rb +6 -1
- data/lib/pubid/asme/parser.rb +41 -14
- data/lib/pubid/astm/CLAUDE.md +9 -0
- data/lib/pubid/astm/builder.rb +2 -0
- data/lib/pubid/astm/components/code.rb +2 -0
- data/lib/pubid/astm/parser.rb +4 -1
- data/lib/pubid/bipm/CLAUDE.md +11 -0
- data/lib/pubid/bipm/builder.rb +2 -0
- data/lib/pubid/bsi/CLAUDE.md +93 -0
- data/lib/pubid/bsi/builder.rb +13 -11
- data/lib/pubid/bsi/identifiers/addendum_document.rb +2 -0
- data/lib/pubid/bsi/identifiers/adopted_european_norm.rb +6 -54
- data/lib/pubid/bsi/identifiers/adopted_international_standard.rb +5 -22
- data/lib/pubid/bsi/identifiers/amendment.rb +36 -12
- data/lib/pubid/bsi/identifiers/bundled_identifier.rb +2 -0
- data/lib/pubid/bsi/identifiers/consolidated_identifier.rb +23 -26
- data/lib/pubid/bsi/identifiers/corrigendum.rb +29 -12
- data/lib/pubid/bsi/identifiers/expert_commentary.rb +6 -7
- data/lib/pubid/bsi/identifiers/national_annex.rb +18 -20
- data/lib/pubid/bsi/identifiers/root_identity.rb +31 -0
- data/lib/pubid/bsi/identifiers/set.rb +2 -0
- data/lib/pubid/bsi/identifiers/supplement_document.rb +2 -0
- data/lib/pubid/bsi/identifiers.rb +1 -0
- data/lib/pubid/bsi/parser.rb +8 -8
- data/lib/pubid/bsi/renderer.rb +20 -20
- data/lib/pubid/bsi/urn_generator.rb +28 -18
- data/lib/pubid/builder/base.rb +27 -0
- data/lib/pubid/calconnect/builder.rb +2 -0
- data/lib/pubid/calconnect/identifier.rb +4 -0
- data/lib/pubid/ccsds/builder.rb +2 -0
- data/lib/pubid/ccsds/identifier.rb +8 -0
- data/lib/pubid/cen_cenelec/CLAUDE.md +59 -0
- data/lib/pubid/cen_cenelec/builder.rb +6 -1
- data/lib/pubid/cen_cenelec/identifier.rb +10 -27
- data/lib/pubid/cen_cenelec/identifiers/amendment.rb +3 -10
- data/lib/pubid/cen_cenelec/identifiers/corrigendum.rb +3 -10
- data/lib/pubid/cen_cenelec/parser.rb +11 -3
- data/lib/pubid/cie/CLAUDE.md +58 -0
- data/lib/pubid/cie/builder.rb +2 -0
- data/lib/pubid/cie/components/language.rb +2 -0
- data/lib/pubid/components/adoption.rb +2 -0
- data/lib/pubid/components/code.rb +2 -0
- data/lib/pubid/components/date.rb +8 -6
- data/lib/pubid/components/edition.rb +2 -0
- data/lib/pubid/components/iteration.rb +2 -0
- data/lib/pubid/components/language.rb +2 -0
- data/lib/pubid/components/locality.rb +2 -0
- data/lib/pubid/components/publisher.rb +2 -0
- data/lib/pubid/components/relationship.rb +2 -0
- data/lib/pubid/components/stage.rb +2 -0
- data/lib/pubid/components/supplement.rb +2 -0
- data/lib/pubid/components/type.rb +2 -0
- data/lib/pubid/components/typed_stage.rb +8 -0
- data/lib/pubid/csa/CLAUDE.md +41 -0
- data/lib/pubid/csa/builder.rb +2 -0
- data/lib/pubid/csa/identifier.rb +13 -1
- data/lib/pubid/csa/renderer.rb +12 -12
- data/lib/pubid/csa/single_identifier.rb +17 -0
- data/lib/pubid/doi/builder.rb +2 -0
- data/lib/pubid/easc/builder.rb +2 -0
- data/lib/pubid/ecma/CLAUDE.md +28 -0
- data/lib/pubid/ecma/builder.rb +2 -0
- data/lib/pubid/ecma/identifier.rb +7 -0
- data/lib/pubid/etsi/CLAUDE.md +34 -0
- data/lib/pubid/etsi/builder.rb +2 -0
- data/lib/pubid/etsi/components/code.rb +6 -0
- data/lib/pubid/etsi/components/version.rb +2 -0
- data/lib/pubid/etsi/identifiers/etsi_standard.rb +7 -0
- data/lib/pubid/evs/CLAUDE.md +58 -0
- data/lib/pubid/evs/builder.rb +2 -0
- data/lib/pubid/gb/CLAUDE.md +140 -0
- data/lib/pubid/gb/builder.rb +7 -2
- data/lib/pubid/gb/identifier.rb +5 -3
- data/lib/pubid/gb/identifiers/all_parts.rb +17 -0
- data/lib/pubid/gb/identifiers.rb +1 -0
- data/lib/pubid/gb/renderer.rb +0 -1
- data/lib/pubid/gost/CLAUDE.md +64 -0
- data/lib/pubid/gost/builder.rb +3 -1
- data/lib/pubid/gost/identifier.rb +5 -0
- data/lib/pubid/iala/CLAUDE.md +82 -0
- data/lib/pubid/iala/builder.rb +2 -0
- data/lib/pubid/iana/CLAUDE.md +7 -0
- data/lib/pubid/iana/builder.rb +2 -0
- data/lib/pubid/identifier.rb +161 -17
- data/lib/pubid/idf/builder.rb +6 -1
- data/lib/pubid/idf/identifier.rb +5 -0
- data/lib/pubid/idf/identifiers/all_parts.rb +17 -0
- data/lib/pubid/idf/identifiers.rb +1 -0
- data/lib/pubid/iec/CLAUDE.md +31 -0
- data/lib/pubid/iec/builder.rb +7 -1
- data/lib/pubid/iec/components/consolidated_amendment.rb +4 -0
- data/lib/pubid/iec/components/sheet.rb +2 -0
- data/lib/pubid/iec/components/trf_info.rb +2 -0
- data/lib/pubid/iec/components/vap_suffix.rb +2 -0
- data/lib/pubid/iec/identifier.rb +7 -2
- data/lib/pubid/iec/identifiers/all_parts.rb +19 -0
- data/lib/pubid/iec/identifiers.rb +1 -0
- data/lib/pubid/iec/renderer.rb +0 -1
- data/lib/pubid/iec/urn_generator.rb +9 -1
- data/lib/pubid/iec/urn_parser.rb +3 -2
- data/lib/pubid/ieee/CLAUDE.md +97 -0
- data/lib/pubid/ieee/builder.rb +134 -5
- data/lib/pubid/ieee/components/code.rb +2 -0
- data/lib/pubid/ieee/components/draft.rb +35 -2
- data/lib/pubid/ieee/components/typed_stage.rb +2 -0
- data/lib/pubid/ieee/identifiers/base.rb +20 -0
- data/lib/pubid/ieee/identifiers/iec_ieee_copublished.rb +9 -0
- data/lib/pubid/ieee/identifiers/joint_development.rb +17 -10
- data/lib/pubid/ieee/identifiers/project_draft_identifier.rb +8 -1
- data/lib/pubid/ieee/parser.rb +134 -21
- data/lib/pubid/ieee/renderer.rb +40 -7
- data/lib/pubid/ieee/urn_generator.rb +31 -0
- data/lib/pubid/ietf/CLAUDE.md +7 -0
- data/lib/pubid/ietf/builder.rb +2 -0
- data/lib/pubid/iho/builder.rb +2 -0
- data/lib/pubid/isbn/builder.rb +2 -0
- data/lib/pubid/iso/CLAUDE.md +47 -0
- data/lib/pubid/iso/builder.rb +19 -5
- data/lib/pubid/iso/components/publisher.rb +2 -0
- data/lib/pubid/iso/identifier.rb +6 -14
- data/lib/pubid/iso/identifiers/all_parts.rb +19 -0
- data/lib/pubid/iso/identifiers/directives_supplement.rb +4 -2
- data/lib/pubid/iso/identifiers.rb +1 -0
- data/lib/pubid/iso/rendering_style.rb +0 -1
- data/lib/pubid/itu/CLAUDE.md +69 -0
- data/lib/pubid/itu/builder.rb +2 -0
- data/lib/pubid/itu/components/code.rb +2 -0
- data/lib/pubid/itu/components/designation.rb +2 -0
- data/lib/pubid/itu/components/sector.rb +2 -0
- data/lib/pubid/itu/components/series.rb +2 -0
- data/lib/pubid/itu/identifiers/supplement.rb +15 -0
- data/lib/pubid/jcgm/CLAUDE.md +7 -0
- data/lib/pubid/jcgm/builder.rb +2 -0
- data/lib/pubid/jcgm/components/publisher.rb +2 -0
- data/lib/pubid/jis/builder.rb +5 -1
- data/lib/pubid/jis/identifier.rb +5 -17
- data/lib/pubid/jis/identifiers/all_parts.rb +19 -0
- data/lib/pubid/jis/identifiers.rb +1 -0
- data/lib/pubid/jis/renderer.rb +0 -2
- data/lib/pubid/jis/urn_generator.rb +0 -1
- data/lib/pubid/nist/CLAUDE.md +56 -0
- data/lib/pubid/nist/builder.rb +2 -0
- data/lib/pubid/nist/components/edition.rb +2 -0
- data/lib/pubid/nist/components/issue_number.rb +2 -0
- data/lib/pubid/nist/components/part.rb +2 -0
- data/lib/pubid/nist/components/stage.rb +2 -0
- data/lib/pubid/nist/components/supplement.rb +2 -0
- data/lib/pubid/nist/components/translation.rb +2 -0
- data/lib/pubid/nist/components/update.rb +2 -0
- data/lib/pubid/nist/components/version.rb +2 -0
- data/lib/pubid/nist/components/volume.rb +2 -0
- data/lib/pubid/nist/identifiers/base.rb +24 -3
- data/lib/pubid/nist/parser.rb +15 -2
- data/lib/pubid/nist/preprocessor.rb +53 -2
- data/lib/pubid/oasis/CLAUDE.md +19 -0
- data/lib/pubid/oasis/builder.rb +2 -0
- data/lib/pubid/oasis/identifier.rb +19 -0
- data/lib/pubid/ogc/CLAUDE.md +34 -0
- data/lib/pubid/ogc/builder.rb +2 -0
- data/lib/pubid/ogc/identifier.rb +11 -0
- data/lib/pubid/oiml/CLAUDE.md +189 -0
- data/lib/pubid/oiml/builder.rb +20 -0
- data/lib/pubid/oiml/components/code.rb +6 -0
- data/lib/pubid/oiml/identifier.rb +13 -0
- data/lib/pubid/oiml/identifiers/annex.rb +4 -0
- data/lib/pubid/oiml/identifiers/certification_system.rb +34 -0
- data/lib/pubid/oiml/identifiers/code_number.rb +8 -0
- data/lib/pubid/oiml/identifiers/dual_published.rb +174 -0
- data/lib/pubid/oiml/identifiers.rb +2 -0
- data/lib/pubid/oiml/parser.rb +35 -4
- data/lib/pubid/oiml/renderer.rb +23 -1
- data/lib/pubid/oiml/single_identifier.rb +4 -0
- data/lib/pubid/oiml/supplement_identifier.rb +7 -0
- data/lib/pubid/oiml/urn_generator.rb +28 -0
- data/lib/pubid/oiml.rb +5 -0
- data/lib/pubid/omg/CLAUDE.md +15 -0
- data/lib/pubid/omg/builder.rb +2 -0
- data/lib/pubid/parser/grammar.rb +23 -0
- data/lib/pubid/plateau/builder.rb +2 -0
- data/lib/pubid/plateau/identifiers/base.rb +4 -0
- data/lib/pubid/plateau/supplement_identifier.rb +14 -2
- data/lib/pubid/plateau/urn_generator.rb +7 -1
- data/lib/pubid/renderers/human_readable.rb +0 -1
- data/lib/pubid/sae/builder.rb +2 -0
- data/lib/pubid/sae/components/date.rb +2 -0
- data/lib/pubid/sae/components/type.rb +2 -0
- data/lib/pubid/subset_match.rb +197 -0
- data/lib/pubid/tgpp/CLAUDE.md +43 -0
- data/lib/pubid/tgpp/builder.rb +2 -0
- data/lib/pubid/tgpp/identifier.rb +14 -0
- data/lib/pubid/type_resolver.rb +14 -2
- data/lib/pubid/un/builder.rb +2 -0
- data/lib/pubid/version.rb +1 -1
- data/lib/pubid/w3c/CLAUDE.md +7 -0
- data/lib/pubid/w3c/builder.rb +2 -0
- data/lib/pubid/xsf/CLAUDE.md +11 -0
- data/lib/pubid/xsf/builder.rb +2 -0
- data/lib/pubid.rb +16 -3
- metadata +44 -2
|
@@ -5,23 +5,49 @@ module Pubid
|
|
|
5
5
|
# Parses AMCA URNs back into identifiers.
|
|
6
6
|
#
|
|
7
7
|
# UrnGenerator emits:
|
|
8
|
-
# urn:amca:{number}:{year}[:{
|
|
8
|
+
# urn:amca:{number}[:{year}][:{keyed tokens}][:{type}]
|
|
9
9
|
#
|
|
10
|
-
# The
|
|
11
|
-
#
|
|
12
|
-
#
|
|
10
|
+
# The keyed tokens are `interp.<code>`, `rev.<revision>`,
|
|
11
|
+
# `reaff.<year>` and `copub.<publisher>`; the type is the key of the
|
|
12
|
+
# identifier class. The parser rebuilds the printed form from them and
|
|
13
|
+
# parses that, so the identifier comes back as the same class.
|
|
13
14
|
#
|
|
14
15
|
# Examples:
|
|
15
|
-
# - urn:amca:210:08
|
|
16
|
+
# - urn:amca:210:08 → AMCA 210-08
|
|
17
|
+
# - urn:amca:211:22:rev.01-23:copub.amca:publication
|
|
18
|
+
# → AMCA Publication 211-22 (Rev. 01-23)
|
|
19
|
+
# - urn:amca:99:interp.jw:copub.amca:interpretation → AMCA 99 JW Interp
|
|
16
20
|
class UrnParser < Pubid::UrnParser::Base
|
|
21
|
+
TYPE_TITLES = {
|
|
22
|
+
"standard" => "Standard",
|
|
23
|
+
"publication" => "Publication",
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
17
26
|
def parse_urn(urn)
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
number, *rest = split_parts(strip_namespace(urn))
|
|
28
|
+
year = rest.shift if rest.first&.match?(/\A\d+\z/)
|
|
29
|
+
keyed = rest.grep(/\./).to_h { |token| token.split(".", 2) }
|
|
30
|
+
type = (rest - keyed.map { |k, v| "#{k}.#{v}" }).last
|
|
31
|
+
|
|
32
|
+
flavor_parse(printed(number, year, keyed, type))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
20
36
|
|
|
21
|
-
|
|
22
|
-
|
|
37
|
+
def printed(number, year, keyed, type)
|
|
38
|
+
publisher = keyed.fetch("copub", "amca").upcase
|
|
39
|
+
return interpretation(publisher, number, keyed) if type == "interpretation"
|
|
40
|
+
|
|
41
|
+
text = [publisher, TYPE_TITLES[type], number].compact.join(" ")
|
|
23
42
|
text += "-#{year}" if year
|
|
24
|
-
|
|
43
|
+
text += " (Rev. #{keyed['rev']})" if keyed["rev"]
|
|
44
|
+
text += " (R#{keyed['reaff']})" if keyed["reaff"]
|
|
45
|
+
text
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def interpretation(publisher, number, keyed)
|
|
49
|
+
code = keyed["interp"]&.upcase
|
|
50
|
+
[publisher, number, code, "Interp"].compact.join(" ")
|
|
25
51
|
end
|
|
26
52
|
end
|
|
27
53
|
end
|
data/lib/pubid/ansi/builder.rb
CHANGED
|
@@ -45,6 +45,10 @@ module Pubid
|
|
|
45
45
|
value.to_s.split(",").map do |lang|
|
|
46
46
|
Components::Language.new(code: lang.strip)
|
|
47
47
|
end
|
|
48
|
+
when :all_parts
|
|
49
|
+
# The shared Grammar marks the tree; the prepended AllPartsWrap
|
|
50
|
+
# routes the wrap, so the marker itself carries no component.
|
|
51
|
+
nil
|
|
48
52
|
else
|
|
49
53
|
raise ArgumentError, "Unknown parameter type: #{type}"
|
|
50
54
|
end
|
|
@@ -52,3 +56,5 @@ module Pubid
|
|
|
52
56
|
end
|
|
53
57
|
end
|
|
54
58
|
end
|
|
59
|
+
|
|
60
|
+
Pubid::Ansi::Builder.prepend(Pubid::Builder::AllPartsWrap)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# API flavor notes
|
|
2
|
+
|
|
3
|
+
API rendering repairs and the `number` retype residue.
|
|
4
|
+
|
|
5
|
+
These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/api/` or `spec/pubid/api/`. The root file keeps the cross-flavor contract that every flavor obeys.
|
|
6
|
+
|
|
7
|
+
## From the root note "`number`/`part`/`subpart` retyped to `:string` — tranche 1 of 3 (ansi, api, bsi, cen_cenelec, idf, jcgm)"
|
|
8
|
+
|
|
9
|
+
**The six flavors have no fixture net of their own**: every one of their `fixtures_spec.rb` files reports **0 examples** (the `../../../fixtures` glob has one `..` too many; api and idf also use an uppercase directory — hand-off `ten-dead-fixture-specs`), and ANSI's sole assertion is `expect(fixtures.size).to be > 0`. Fixing those globs would drag in unrelated stale-fixture failures, so the new spec's corpus sweep reads the same files directly — the ashrae/csa/etsi/oiml precedent. **One pre-existing defect repaired in passing**: `API COS 1-07/RP 75, 4th edition` put a raw `Parslet::Slice` into `root.number` (the hand-off warned it would look like a migration failure); `Api::Builder#cast` now returns `value.to_s`, so it is a real String. **One severe API defect repaired here — the ONLY deliberate `to_s` change in this branch.** `Pubid::Api::Identifier.parse("API RP 500").to_s` was `"API"`: **163 of API's 193 corpus ids rendered as the bare publisher**, and since `to_s` is the document number *and* the output filename, all 163 collapsed onto one another. Two independent **dead reads**, both found by pulling on the `code` thread this retype exposed: (1) `Api::Renderer#code_portion` read an `id.code` attribute that **nothing ever assigns** — the parser emits no `:code` key and `Builder#cast` never sets one — while the number the builder does populate sits in `number`; (2) the type token was gated on `id.class.attributes.key?(:type_string)`, but `type_string` is a plain **method** on each leaf, never a lutaml attribute, so that guard was always false. The fix is two reads (`number`, and `respond_to?(:type_string)`), plus deleting the `code` attribute and the three dead `code_portion` methods (`SingleIdentifier`'s and `Mpms`'s were never called at all — the renderer has its own). **`render_mpms` never consulted `code_portion`, which is exactly why MPMS was the one form that always rendered correctly.** Byte-exact `to_s` goes from **29/193 to 190/193**, and the 3 residuals are precisely the normalizing parses the generated fixtures already mark `!input!rendered`. **The strongest evidence that this restores intent rather than inventing output: regenerating the fixtures (`rake "validation:classify[api]"`) changed exactly ONE line** — a stale `!… !API` corrected to `!… !API 75-07`. The corpus had recorded the correct renderings all along; the code had drifted away from them silently, because API's `fixtures_spec.rb` matches no files and reports 0 examples and its other specs are `respond_to?(:parse)` stubs. Re-adding a `code` attribute would silently reopen the defect, so `spec/pubid/api/rendering_gaps_spec.rb` asserts it is absent from the whole hierarchy. **Note `maybe(:code)` in `urn_generator/base.rb#urn_number` is safe** — `maybe` guards on `attributes.key?`, so the removed attribute simply never matches. Measured over the corpus, the repair is **purely additive**: of 193 ids, 163 grew and **0 shrank**, and every new rendering *extends* the old one (`to_urn`, `to_hash` and `root.number` are untouched) — the ASHRAE invariant, which is stronger than byte-identity for a fix. **An MR gap this branch pinned was closed mid-flight by PR #354, and the pin is what caught it** — a worked example of why these "assert the CURRENT behaviour" blocks earn their place. When written, all **30** MPMS ids slugged to the bare `"api"` because MPMS kept its locator in a `chapter` attribute and had no `number` at all. #354 then moved the chapter into the inherited `number` (it *is* the document number) and this branch retyped that `number` to `:string`; **composed, they close the collapse** — 0 ids now slug to bare `"api"`, and `API MPMS CH 12.2` keys `"12"` as a String. Merging `main` turned the pin red exactly as designed, and it was rewritten to assert the repair. **A narrower gap remains and is re-pinned**: the MR slug is built from `number` alone, so the sections of one chapter still share it (`CH 14.4` and `CH 14.6` both → `api.14`, 11 colliding slugs corpus-wide, and `to_slug` is a filename). Closing that needs an `Mpms#mr_number_with_part` appending section/subsection (the BIPM/OIML shape). **Merge note for whoever does tranche 2/3**: #354's `Builder#handle_key` routes the MPMS `:chapter` parse key into the `number` *attribute*, and `assign_attributes` hands `handle_key` the **already-cast** value — so the `cast` arm must be `when :number, :chapter then value.to_s`, not a `Components::Code`, or MPMS silently regains a boxed number. (hand-off: retype-number-to-string; remaining: tranche 2 = CSA, tranche 3 = ISO and NIST on separate branches, then flip `identifier.rb:136-138` and delete the leaf `:string` redeclarations and the twelve tripwire blocks.)
|
|
10
|
+
|
|
11
|
+
## From the root note "Parse-failure error contract — uniform across every flavor"
|
|
12
|
+
|
|
13
|
+
**`Identifier.parse` was the one public parse in the gem that could return `nil`.** `lib/pubid/api/identifier.rb` opened with
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
return nil if input.start_with?("#") # "Filter out comments"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
a **fixture-file concern living in a public API**. A caller passing a `#`-leading string got `nil` back and discovered it as a `NoMethodError` on `.to_s`, far from the cause. `lib/pubid/csa/identifier.rb` even carried a comment noting that every flavor "but `api`" honoured the raise-or-return-an-identifier contract, so this was known and unfixed.
|
|
20
|
+
|
|
21
|
+
Deleting it cost nothing: **both** API fixture loaders already reject `#` lines themselves (`spec/pubid/api/fixtures_spec.rb`, `spec/pubid/api/root_number_spec.rb`), so nothing depended on the nil. A comment line is not an identifier and now raises `Parslet::ParseFailed` like any other unparseable string. The `rescue Parslet::ParseFailed => e; raise e` that followed it was a no-op and went too, and API gained the input guards it had never had (it was one of the 14 flavors an over-long string reached the parser in).
|
|
22
|
+
|
|
23
|
+
**Rule this illustrates:** a filter that exists for *how the test corpus is stored* belongs in the corpus loader, never in `parse`.
|
data/lib/pubid/api/builder.rb
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# ASHRAE flavor notes
|
|
2
|
+
|
|
3
|
+
ASHRAE index key, the identifiers that lost their number, the errata date, and interpretations.
|
|
4
|
+
|
|
5
|
+
These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/ashrae/` or `spec/pubid/ashrae/`. The root file keeps the cross-flavor contract that every flavor obeys.
|
|
6
|
+
|
|
7
|
+
- **ASHRAE index key (`root.number`), and the 246 identifiers that were losing their number**: ASHRAE kept its document code in a `code` :string on the shared base and never set the inherited `number`, so all 2,103 fixture identifiers keyed `""`. Worse, **246 of them had no number to key on**: they parsed *successfully* and rendered as nonsense — 160 Errata as the identical `"ASHRAE Standard Errata"` (note the double space, the BIPM `Declaration` corruption signature), the addendum forms as `"ASHRAE Standard "` or `"… to ASHRAE Standard -2024"`. Triaged over the whole corpus, that was **not one bug but three**, and the hand-off's guess ("the parser discards the number") was wrong about the largest: **(1) The BUILDER, 223 ids — the real cause.** The grammar captured everything; the parse tree for an erratum is `{base: {publisher, type, code, year}, errata_keyword: …}`. But `build_errata` and the addendum builders passed the **whole** parse hash to `#extract_base_attributes`, which reads `parsed[:publisher]`/`[:type]`/`[:code]` — all nil, because they sit under `parsed[:base]`. `Builder#build` unwraps at its own entry (`if parsed_hash[:base]`); the five other call sites never did. Fixed **inside `extract_base_attributes`**, which covers all five at once rather than repeating the unwrap. Two grammar rules — `:addendum` and `:publisher_base_addendum` — additionally had **no dispatch branch at all**, so they fell through to the plain-identifier path, which looks for publisher/type/code at the top of the hash, found only the wrapper key, and built an empty Standard (63 of the 223, which were additionally the WRONG CLASS — a bare `Standard` where an `Addendum` was meant). Both subtrees are flat and carry exactly what `build_addendum_from_identifier` already reads. **(2) The GRAMMAR, 23 ids — one missing capture.** `rule(:code_with_year)` matched the code digits and then **discarded** them: only `year_digits.as(:year)` was named. Wrapping the code portion in `.as(:code)` is the whole fix, and the builder was already reading `code_with_year[:code]` — it had been waiting for a value the grammar never produced. **(3) The index key.** `attribute :code, :string` moved off `Pubid::Ashrae::Identifier` and became `attribute :number, :string` on the **two single-document leaves** (`Standard`, `Guideline`). Leaf placement is load-bearing in the CSA counter-shape here: that base is inherited by **both** `SingleIdentifier` and `SupplementIdentifier`. The five supplement types carry **no number of their own** and reach the key through `#root`, which walks `base` — so `UrnGenerator#urn_number` needed an `identifier.number || identifier.root.number` fallback, without which every supplement URN silently lost its number when the attribute moved. **Landmine when renaming `code` → `number` in a flavor whose builder reads Parslet output: the builder holds BOTH attribute keys and parse-TREE keys under the same name.** A blanket `s/\bcode\b/number/` through `builder.rb` renames `parsed[:code]` too and breaks every lookup; the rename must be surgical (`attributes[:code]`, `base_attrs[:code]`, `code: extract_value`) while `parsed[:code]` / `code_year_data[:code]` stay. (`renderer.rb` and `urn_generator.rb` read only attributes, so a blanket rename is safe there.) Also note **BSD `sed` does not support `\b`** — an `sed -i '' 's/\bcode\b/…/'` on macOS silently changes nothing; use `perl -pi -e`. **(4) The MR slug, the worst collapse measured in the gem: ALL 2,103 ids shared the single slug `"ashrae"`.** Every base hook looked for something ASHRAE does not use (`mr_type` a typed_stage, `mr_year` a `Components::Date`), and with `number` nil too there was nothing left. The base now supplies `mr_type`/`mr_year`/`mr_publisher`/`mr_number_with_part`/`mr_sanitize`, and the four **reachable** supplement types define `mr_supplement_suffix` so `Renderers::MrString` recurses into `base` instead of slugging flat. Every identity-bearing marker was chased in until distinct identifiers got distinct slugs: `type` (Guideline 0 ≠ Standard 0), `copublisher` (`ANSI/ASHRAE` ≠ `ASHRAE` — their hashes differ, so their filenames must), `reaffirmed` (`RA 2017`), `suffix` (`R`/`P`), and the addendum letters. `mr_sanitize` filters **by charset** (the BIPM precedent): an ASHRAE code carries commas (`90A,B,C`) and the copublisher a slash, both outside the `[a-z0-9.-]` charset `MrString` documents. Result: **2,062 distinct slugs for 2,103 ids, 0 unsafe characters, 0 clashes between distinct identifiers** (the 41 collapses are pairs whose hashes are genuinely equal). **Verified** by replaying a baseline captured on `main` over every real fixture input: `to_s` **shrank for 0 of 2,103** (every rendering only gained content — the right invariant for a repair, stronger than byte-identity), 0 empty `root.number` (was 2,103), 0 renderings with a double space (was 160) or without a digit (was 223), 0 round-trip failures, and `to_urn` changed for exactly the 246 repaired ids (243 gained their number; 3 replaced the garbage `urn:ashrae:standard`, which carried only a wrongly-defaulted type, with `urn:ashrae:28`). **COUNT IN IDENTIFIERS, NOT FIXTURE LINES**: the first revision of this entry said 3,619 and 489, because a `pass/` line for a normalizing parse is written `!input!rendered` and the ASHRAE grammar is loose enough to *parse* that doubled string — so 1,518 of them were counted, and swept, as identifiers. `spec/pubid/ashrae/root_number_spec.rb` now splits the marker. **Two review findings worth keeping.** The `code` -> `number` rename left `SupplementIdentifier#code` (`def code; base&.code; end`) pointing at a method that no longer exists, so it **raised NoMethodError on every supplement instance** — and nothing inside `lib/pubid/ashrae` calls it, which is exactly why the whole suite stayed green while a public method on five types was broken. Its body is now `base&.number`, and **the method is deliberately still named `code`**: renaming it to `number` looks tidier and is wrong twice over — `number` is an inherited lutaml attribute typed `Components::Code`, so a String-returning method of that name makes `to_hash` raise `IncorrectModelError` on every supplement, and even typed correctly it would serialize a duplicate of the base's number onto the wrapper. (The same trap as the ASTM `Adjunct#number` above; `code` is free to be a plain reader precisely because it is not an attribute anywhere.) Second, `mr_number_with_part` now goes through the shared `#mr_join` rather than a bare `compact.join("-")`, so it degrades to nil rather than `""`. **Three known gaps, all pre-existing and all pinned by specs asserting the CURRENT behaviour, so a fix trips them rather than passing silently.** **(a) Interpretations — FIXED since (see the interpretation bullet below).** An `"Interpretations for Standard 15.2-2022"` reference builds a plain `Standard` **identical** to the standard it interprets (same class, `to_s`, hash, URN and slug), and all 51 corpus rows already behaved this way on `main`. **The cause is NOT alternation ordering** — the first revision of this entry said it was. `rule(:interpretation_identifier)` *is* reached and *does* match (dump the tree: `{base: {type: "Standard", code: "15.2", year: "2022"}}`, prefix consumed); what it never does is **tag** itself, having no `.as(:interpretation_identifier)` wrapper — so `Builder#build`'s `elsif parsed_hash[:interpretation_identifier]` is unreachable dead code and the tree falls through to the plain path. `Identifiers::Interpretation` is therefore unreachable, which is why it alone got no `mr_supplement_suffix`. **(b) The errata date — FIXED since (see the errata-date bullet below).** `Addendum#addendum_date` still has the always-nil extractor (making `render_addendum`'s date branch dead), and `AddendaPackage`'s `package_year`/`target_year` and `CombinedAddenda`'s `connector` are still captured by the grammar and never read by the builder. **(c) The supplement URN — FIXED since (see the supplement-URN bullet below).** **Also stale, deliberately**: `spec/fixtures/ashrae/identifiers/pass/*.txt` was generated on `main`, so it still records the corrupt renderings as expected output (`!… Errata (September 28, 2011) (PDF)!ASHRAE Standard Errata`). Inert while `fixtures_spec.rb`'s glob is dead, but it will fail the moment that glob is fixed; regenerate with `bundle exec rake "validation:classify[ashrae]"`, which rewrites `pass/` and `fail/` wholesale (the CSA precedent above). Locked by `spec/pubid/ashrae/root_number_spec.rb` — structural tripwire (the two numbered leaves resolve `number` to `String`, the three inherited-from classes still resolve to `Components::Code`, and the supplements deliberately have no number of their own), a recovered-identifier table naming one example per defect shape, and a corpus sweep asserting non-empty keys, no double space, at least one digit, round-trip, and slug distinctness. **That sweep is ASHRAE's only fixture net**: `spec/pubid/ashrae/fixtures_spec.rb` globs `../../../fixtures/ASHRAE/…` and reports 0 examples (hand-off `ten-dead-fixture-specs`). (hand-off: ashrae-parser-drops-the-number; follow-ups: ashrae-interpretation-collapses-onto-base, ashrae-errata-date-dropped, ashrae-supplement-urn-collapse.)
|
|
8
|
+
|
|
9
|
+
- **The errata date is captured, because ASHRAE identifies an erratum by its date.** `ASHRAE Guideline 14-2002 Errata (October 10, 2008)` and `… (October 20, 2008)` are two documents with different content (the October 10 sheet applies to all copies; the October 20 sheet applies only to the downloaded PDF). Before this change they had one `to_s`, `to_hash`, `==` and slug. **There were three causes, and the hand-off named only two.** The builder stub returned nil, and the rule named only `:errata_year`. The third cause is the one to remember: `rule(:errata_date)` started with `lparen` and had **no leading space**, so after `Errata` it never matched, and `optional_suffix` (a generic `" (…)"` eater) took the date instead. The parse tree showed it: `optional_suffixes: " (October 10, 2008)"`. **A generic suffix rule after an optional capture hides a capture that does not match**: the parse still succeeds, so no test fails. The rule now starts with `space.maybe` and names `:month`, `:day`, `:errata_year` and `:numeric_date`. The three `errata_identifier` branches now share one `rule(:errata_date_and_suffix)`: the plain-publisher branch had only the date-first order, so `ASHRAE Standard 55-2004 Errata – Spanish Edition (June 2, 2010)` lost its date the same way (a code review found it; no corpus row has that shape). **The date lives in the `date` component `Errata` inherits, not in a flavor-named attribute.** The first version of this fix (PR #405) declared `attribute :errata_date, :string`. That is the GB shape: two attributes describing one value, and the flavor-named one is invisible to every shared surface, which reads `date`. `Builder#extract_errata_date` now returns a `Components::Date`, so one date has one value whatever the input spelled — `October 10, 2008`, `October 10,2008` and `10-10-2008` all give `year: "2008", month: "10", day: "10"`. **The month and the day are padded to two digits**, so two identifiers cannot differ by a leading zero; `Renderer#long_date` inverts the mapping and prints the day unpadded, in the long form the ASHRAE PDF headers use. `to_s` is byte-identical to the string form for every corpus row, and a numeric date now prints as `(July 17, 2003)`. The slug takes the sortable form: `ashrae.guideline.14.2002_errata.2008-10-10`. **One override was needed, and it is the price of using the shared name**: `UrnGenerator::Base#urn_year` reads `date` first, so an erratum would have emitted `urn:ashrae:14:2008` — the erratum year in the slot that holds the edition year of the standard. `Ashrae::UrnGenerator#urn_year` reads the `year` attribute only, and every ASHRAE URN stays as it was. A second consequence is intended and recorded in the spec: the base `#exclude` adds `:date` to `:year`, so `matches?(other, ignore: [:year])` now ignores the erratum date as well. **Measured** by a replay of all 2,103 inputs in `spec/fixtures/ashrae/identifiers/full/` against a `main` baseline: exactly the 160 `Errata` rows changed `to_s`, `to_hash` and slug; every changed `to_s` only gained the date; the class and `to_urn` did not change for any row; `from_hash(to_hash) == id` holds for all. **Not changed, on purpose:** the URN, which had no supplement marker at the time (fixed since — see the supplement-URN bullet below); the 34 `Addendum … Errata (date)` inputs, which parse through `errata_suffix_on_addendum` and build a plain `Addendum` without the errata; and the `– Spanish Edition` errata suffix, which is still dropped. **A bare `ASHRAE Guideline 14-2002 Errata` still parses, with `date` nil** (the partial-reference contract), and `matches?(other, ignore: [:date])` matches every erratum of the standard. It names no single document, and neither erratum supersedes the other, so **a consumer must treat a bare reference as ambiguous and must not resolve it to the latest erratum**. No relaton consumer of ASHRAE exists today. Locked by `spec/pubid/ashrae/identifiers/errata_spec.rb` and the flipped block "two errata of one standard are two identifiers" in `spec/pubid/ashrae/root_number_spec.rb`. (hand-off: ashrae-errata-date-dropped.)
|
|
10
|
+
|
|
11
|
+
- **An interpretation is an `Interpretation`, not the standard it interprets.** `Interpretations for Standard 15.2-2022` built a plain `Standard` with the `to_s`, `to_hash`, `==` and slug of `ASHRAE Standard 15.2-2022`, for all 51 corpus rows. The builder, the class, the renderer and `mr_supplement_suffix` were all correct; only the grammar tag was missing. `rule(:interpretation_identifier)` now wraps its output in `.as(:interpretation_identifier)`, so the `Builder#build` branch for that key is reached. **Lesson: a `Builder#build` branch that dispatches on a parse-tree key is dead code unless some rule emits that key** — grep the parser for the `.as(:key)` before you trust the branch. The `:addendum_identifier` branch has the same shape; it is harmless, because each sub-alternative emits its own key and has its own branch. **Measured** by a replay of all 2,103 inputs against a `main` baseline: exactly the 51 interpretation rows changed; each renders byte-exact (`Interpretations for Standard 15.2-2022`), keeps its `root.number`, round-trips through `from_hash`, and has a unique slug (`ashrae.standard.15.2.2022_interp`). The URN changed from the standard's own URN to the bare supplement URN `urn:ashrae:15.2`, which every other supplement of that base shared; it is now `urn:ashrae:15.2:2022:standard:interp` (see the supplement-URN bullet below). The interpretations still sit in `spec/fixtures/ashrae/identifiers/pass/standard.txt` until the next `validation:classify[ashrae]` run. Locked by the flipped block "interpretations are distinct from their base standard" in `spec/pubid/ashrae/root_number_spec.rb`. (hand-off: ashrae-interpretation-collapses-onto-base.)
|
|
12
|
+
|
|
13
|
+
- **A supplement URN is the URN of its base plus one supplement marker.** `UrnGenerator#generate` read `year`, `type` and `suffix` off the identifier, and a supplement keeps them on `base`. So every Addendum, Errata, Interpretation and package of one document shared the bare `urn:ashrae:<number>`, which was not the base's own URN either. `urn_addendum` read `maybe(:addendum)`, an attribute no class declares, so the addendum letter never reached a URN. `#generate` now sends a supplement with a `base` to `#supplement_urn`: the base URN (recursive, through `self.class.new(base)`), then `mr_supplement_suffix` — `urn:ashrae:28:2016:guideline:add.e`, `urn:ashrae:14:2002:guideline:errata.2008-10-10`, `urn:ashrae:15.2:2022:standard:interp`. **The marker is the MR slug marker on purpose**, so the two identity surfaces cannot drift, and it is already filtered to `[a-z0-9.-]`. The dead `urn_addendum` is gone. A supplement `copublisher` equals its base `copublisher` for every corpus row, so the base URN loses nothing; the `copub.` segment now sits after the year and the type, where the base puts it. **Measured** by a replay of all 2,103 inputs against a `main` baseline: `to_s`, `to_hash`, the class and the slug did not change for any row; the URN changed for exactly the 1,649 supplements; distinct URNs went from 562 to 2,088, and URNs shared by different hashes went from 71 to 0. No `relaton-data-ashrae` exists, so no published index holds an ASHRAE URN. **Not changed:** `UrnParser` still reads only `number:year` into a plain `Standard`, so a supplement URN does not parse back (it did not before either); and a base number or copublisher can put a comma, a slash or a capital letter into a URN (`urn:ashrae:90A,B,C:standard`, `copub.ansi/ashrae`). Locked by `spec/pubid/ashrae/supplement_urn_spec.rb` and the corpus example "gives distinct identifiers distinct URNs" in `spec/pubid/ashrae/root_number_spec.rb`. (hand-off: ashrae-supplement-urn-collapse.)
|
data/lib/pubid/ashrae/builder.rb
CHANGED
|
@@ -5,6 +5,13 @@ module Pubid
|
|
|
5
5
|
# Builder class for constructing ASHRAE identifier scheme from parsed data
|
|
6
6
|
# Single Responsibility: Transform parsed data into identifier objects
|
|
7
7
|
class Builder
|
|
8
|
+
# Month names, in the order the parser matches them. The index gives the
|
|
9
|
+
# month number the date component stores, and the renderer inverts it.
|
|
10
|
+
MONTH_NAMES = %w[
|
|
11
|
+
January February March April May June July August September October
|
|
12
|
+
November December
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
8
15
|
attr_reader :identifier_class
|
|
9
16
|
|
|
10
17
|
def initialize(identifier_class = Identifier)
|
|
@@ -172,11 +179,9 @@ module Pubid
|
|
|
172
179
|
base_class = determine_identifier_class(base_attrs)
|
|
173
180
|
base = base_class.new(**base_attrs)
|
|
174
181
|
|
|
175
|
-
errata_date = extract_errata_date(parsed[:errata_date])
|
|
176
|
-
|
|
177
182
|
Identifiers::Errata.new(
|
|
178
183
|
base: base,
|
|
179
|
-
|
|
184
|
+
date: extract_errata_date(parsed[:errata_date]),
|
|
180
185
|
)
|
|
181
186
|
end
|
|
182
187
|
|
|
@@ -388,7 +393,10 @@ module Pubid
|
|
|
388
393
|
|
|
389
394
|
# Build the addendum_codes string from first code and additional codes
|
|
390
395
|
first_code = extract_value(parsed[:addendum_code])
|
|
396
|
+
# The "a through z" range form yields a single Hash, not an
|
|
397
|
+
# Array; iterating it would emit the key name into the codes.
|
|
391
398
|
additional_codes = parsed[:additional_codes]
|
|
399
|
+
additional_codes = [additional_codes] if additional_codes.is_a?(Hash)
|
|
392
400
|
|
|
393
401
|
# Handle Format 4: "ASHRAE Addenda to Standard" (no specific codes)
|
|
394
402
|
if first_code.nil? && additional_codes.nil?
|
|
@@ -490,19 +498,53 @@ module Pubid
|
|
|
490
498
|
attributes
|
|
491
499
|
end
|
|
492
500
|
|
|
493
|
-
# Extract errata date
|
|
501
|
+
# Extract the errata date into the inherited `date` component.
|
|
502
|
+
# The component holds numbers, so every spelling of one date — "October
|
|
503
|
+
# 10, 2008", "October 10,2008", "10-10-2008" — gives one value, and the
|
|
504
|
+
# renderer writes the long form ASHRAE prints. The month and the day are
|
|
505
|
+
# padded to two digits, so two identifiers cannot differ by a leading
|
|
506
|
+
# zero. A date with no year keeps the month and the day.
|
|
494
507
|
# @param errata_date [Hash] the parsed errata_date hash
|
|
495
|
-
# @return [
|
|
508
|
+
# @return [Pubid::Components::Date, nil] the date, or nil when absent
|
|
496
509
|
def extract_errata_date(errata_date)
|
|
497
|
-
return nil unless errata_date
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
extract_value(errata_date[:
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
510
|
+
return nil unless errata_date.is_a?(Hash)
|
|
511
|
+
|
|
512
|
+
numeric = extract_value(errata_date[:numeric_date])
|
|
513
|
+
return numeric_errata_date(numeric) if numeric
|
|
514
|
+
|
|
515
|
+
month = month_number(extract_value(errata_date[:month]))
|
|
516
|
+
day = extract_value(errata_date[:day])
|
|
517
|
+
return nil unless month && day
|
|
518
|
+
|
|
519
|
+
date(extract_value(errata_date[:errata_year]), month, day)
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# "7-17-2003" (the parser also accepts "7-17- 2003") is month, day, year.
|
|
523
|
+
# @param value [String] the numeric date as written
|
|
524
|
+
# @return [Pubid::Components::Date]
|
|
525
|
+
def numeric_errata_date(value)
|
|
526
|
+
month, day, year = value.delete(" ").split("-")
|
|
527
|
+
date(year, month, day)
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
# @param name [String, nil] a month name the parser matched
|
|
531
|
+
# @return [Integer, nil] the month number
|
|
532
|
+
def month_number(name)
|
|
533
|
+
index = MONTH_NAMES.index(name)
|
|
534
|
+
index && index + 1
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
# @return [Pubid::Components::Date] the date, with padded month and day
|
|
538
|
+
def date(year, month, day)
|
|
539
|
+
Pubid::Components::Date.new(
|
|
540
|
+
year: year, month: pad2(month), day: pad2(day),
|
|
541
|
+
)
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# @param value [String, Integer] a month or day number
|
|
545
|
+
# @return [String] the number with two digits
|
|
546
|
+
def pad2(value)
|
|
547
|
+
value.to_s.to_i.to_s.rjust(2, "0")
|
|
506
548
|
end
|
|
507
549
|
|
|
508
550
|
# Extract addendum date from parsed addendum_year data
|
|
@@ -517,3 +559,5 @@ module Pubid
|
|
|
517
559
|
end
|
|
518
560
|
end
|
|
519
561
|
end
|
|
562
|
+
|
|
563
|
+
Pubid::Ashrae::Builder.prepend(Pubid::Builder::AllPartsWrap)
|
|
@@ -11,17 +11,29 @@ module Pubid
|
|
|
11
11
|
# - ASHRAE Guideline 0-2005 Errata (September 28, 2011)
|
|
12
12
|
# - ANSI/ASHRAE Standard 62.1-2004 Errata (May 4, 2007)
|
|
13
13
|
class Errata < SupplementIdentifier
|
|
14
|
-
|
|
14
|
+
# The date of the erratum lives in the `date` component this class
|
|
15
|
+
# inherits from ::Pubid::Identifier. A flavor-named second attribute
|
|
16
|
+
# (the old `errata_date` :string) described the same value under a
|
|
17
|
+
# name no shared code reads — the GB lesson in CLAUDE.md. The base
|
|
18
|
+
# standard keeps its own edition year in `base.year`.
|
|
15
19
|
|
|
16
20
|
# Make Renderers::MrString recurse into `base` instead of slugging the
|
|
17
21
|
# supplement flat off attributes it does not have. Without it every
|
|
18
22
|
# erratum of every standard shared one filename, and `to_slug` is what
|
|
19
23
|
# consumers use as an output filename.
|
|
20
24
|
def mr_supplement_suffix
|
|
21
|
-
["errata", mr_sanitize(
|
|
25
|
+
["errata", mr_sanitize(date_slug)]
|
|
22
26
|
.compact.reject(&:empty?).join(".")
|
|
23
27
|
end
|
|
24
28
|
|
|
29
|
+
# The sortable form of the date, for the slug only: "2008-10-10", or
|
|
30
|
+
# "08-27" for the one shape that carries no year.
|
|
31
|
+
def date_slug
|
|
32
|
+
return nil unless date
|
|
33
|
+
|
|
34
|
+
[date.year, date.month, date.day].compact.join("-")
|
|
35
|
+
end
|
|
36
|
+
|
|
25
37
|
TYPED_STAGES = [
|
|
26
38
|
Components::TypedStage.new(
|
|
27
39
|
abbr: ["Errata"],
|
|
@@ -13,16 +13,8 @@ module Pubid
|
|
|
13
13
|
class Interpretation < SupplementIdentifier
|
|
14
14
|
# Mirrors its four sibling supplement types: without it
|
|
15
15
|
# Renderers::MrString slugs the interpretation FLAT off attributes it
|
|
16
|
-
# does not have, instead of recursing into `base`.
|
|
17
|
-
#
|
|
18
|
-
# Currently unreachable, and NOT because of alternation ordering:
|
|
19
|
-
# rule(:interpretation_identifier) is reached and does match, but it
|
|
20
|
-
# never wraps its output in `.as(:interpretation_identifier)`, so
|
|
21
|
-
# Builder#build's branch for that key is dead code and the tree falls
|
|
22
|
-
# through to the plain-Standard path (pre-existing; pinned in
|
|
23
|
-
# spec/pubid/ashrae/root_number_spec.rb, hand-off
|
|
24
|
-
# ashrae-interpretation-collapses-onto-base). Added anyway so that
|
|
25
|
-
# fixing the dispatch does not silently ship malformed filenames.
|
|
16
|
+
# does not have, instead of recursing into `base`. An interpretation
|
|
17
|
+
# carries no field of its own, so the marker alone is the suffix.
|
|
26
18
|
def mr_supplement_suffix
|
|
27
19
|
"interp"
|
|
28
20
|
end
|
data/lib/pubid/ashrae/parser.rb
CHANGED
|
@@ -120,17 +120,22 @@ module Pubid
|
|
|
120
120
|
str("November") | str("December")
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
+
# The date is what tells two errata of one standard apart, so every part
|
|
124
|
+
# of it is named. The leading space is part of the rule: without it the
|
|
125
|
+
# date never matched after "Errata", and optional_suffix took it instead.
|
|
123
126
|
rule(:errata_date) do
|
|
124
127
|
# Full date: (Month Day, Year)
|
|
125
|
-
(lparen >> month_name >> space >>
|
|
126
|
-
|
|
128
|
+
(space.maybe >> lparen >> month_name.as(:month) >> space >>
|
|
129
|
+
digit.repeat(1, 2).as(:day) >> comma.maybe >> (space | comma.maybe) >>
|
|
127
130
|
year_digits.as(:errata_year) >> rparen).as(:errata_date) |
|
|
128
131
|
# Month+day without year: (August 27)
|
|
129
|
-
(lparen >> month_name >> space >>
|
|
130
|
-
|
|
132
|
+
(space.maybe >> lparen >> month_name.as(:month) >> space >>
|
|
133
|
+
digit.repeat(1, 2).as(:day) >> rparen).as(:errata_date) |
|
|
131
134
|
# Numeric date with dash: (7-17- 2003) or (7-17-2003)
|
|
132
|
-
(
|
|
133
|
-
|
|
135
|
+
(space.maybe >> lparen >>
|
|
136
|
+
(digit.repeat(1, 2) >> dash >> digit.repeat(1, 2) >> dash >>
|
|
137
|
+
space.maybe >> year_digits).as(:numeric_date) >>
|
|
138
|
+
rparen).as(:errata_date)
|
|
134
139
|
end
|
|
135
140
|
|
|
136
141
|
# Errata suffix pattern - handles descriptive text like "– Spanish Edition" after "Errata"
|
|
@@ -141,6 +146,17 @@ module Pubid
|
|
|
141
146
|
)
|
|
142
147
|
end
|
|
143
148
|
|
|
149
|
+
# The date and the descriptive suffix after "Errata", in either order. All
|
|
150
|
+
# three errata_identifier branches share this rule: the publisher branch had
|
|
151
|
+
# only the date-first order, so "Errata – Spanish Edition (June 2, 2010)"
|
|
152
|
+
# lost its date to optional_suffix.
|
|
153
|
+
rule(:errata_date_and_suffix) do
|
|
154
|
+
(errata_suffix >> errata_date) |
|
|
155
|
+
(errata_date >> errata_suffix) |
|
|
156
|
+
errata_date |
|
|
157
|
+
errata_suffix
|
|
158
|
+
end
|
|
159
|
+
|
|
144
160
|
# Errata suffix on addendum (e.g., "ASHRAE Addendum a to Standard 15-2001 Errata (July 6, 2021)")
|
|
145
161
|
rule(:errata_suffix_on_addendum) do
|
|
146
162
|
space >> str("Errata").as(:errata_keyword) >>
|
|
@@ -179,12 +195,7 @@ module Pubid
|
|
|
179
195
|
).as(:base) >>
|
|
180
196
|
space >>
|
|
181
197
|
str("Errata").as(:errata_keyword) >>
|
|
182
|
-
|
|
183
|
-
(errata_suffix >> errata_date) |
|
|
184
|
-
(errata_date >> errata_suffix) |
|
|
185
|
-
errata_date |
|
|
186
|
-
errata_suffix
|
|
187
|
-
).maybe >>
|
|
198
|
+
errata_date_and_suffix.maybe >>
|
|
188
199
|
optional_suffix.repeat(0, 2).as(:optional_suffixes)) |
|
|
189
200
|
# Format with copublisher, missing type: ANSI/ASHRAE 51-1999 Errata (May 23, 2014)
|
|
190
201
|
((
|
|
@@ -201,12 +212,7 @@ module Pubid
|
|
|
201
212
|
).as(:base) >>
|
|
202
213
|
space >>
|
|
203
214
|
str("Errata").as(:errata_keyword) >>
|
|
204
|
-
|
|
205
|
-
(errata_suffix >> errata_date) |
|
|
206
|
-
(errata_date >> errata_suffix) |
|
|
207
|
-
errata_date |
|
|
208
|
-
errata_suffix
|
|
209
|
-
).maybe >>
|
|
215
|
+
errata_date_and_suffix.maybe >>
|
|
210
216
|
optional_suffix.repeat(0, 2).as(:optional_suffixes)) |
|
|
211
217
|
# Format with publisher: ASHRAE Guideline 0-2005 Errata (September 28, 2011)
|
|
212
218
|
((
|
|
@@ -220,16 +226,22 @@ module Pubid
|
|
|
220
226
|
).as(:base) >>
|
|
221
227
|
space >>
|
|
222
228
|
str("Errata").as(:errata_keyword) >>
|
|
223
|
-
|
|
224
|
-
errata_suffix.maybe >>
|
|
229
|
+
errata_date_and_suffix.maybe >>
|
|
225
230
|
optional_suffix.repeat(0, 2).as(:optional_suffixes))
|
|
226
231
|
end
|
|
227
232
|
|
|
228
233
|
# Interpretation pattern ("Interpretations for Standard X-YYYY")
|
|
234
|
+
#
|
|
235
|
+
# The `.as(:interpretation_identifier)` tag is what Builder#build
|
|
236
|
+
# dispatches on. Without it the tree is a bare `{base: …}`, the builder
|
|
237
|
+
# takes the plain-identifier path, and the interpretation becomes the
|
|
238
|
+
# standard it interprets.
|
|
229
239
|
rule(:interpretation_identifier) do
|
|
230
|
-
str("Interpretations") >> space >>
|
|
240
|
+
(str("Interpretations") >> space >>
|
|
231
241
|
str("for") >> space >>
|
|
232
|
-
(type.as(:type) >> space >> code >>
|
|
242
|
+
(type.as(:type) >> space >> code >>
|
|
243
|
+
(dash >> year_digits.as(:year)).maybe).as(:base))
|
|
244
|
+
.as(:interpretation_identifier)
|
|
233
245
|
end
|
|
234
246
|
|
|
235
247
|
# Combined Addenda pattern (multiple addendums grouped together)
|
|
@@ -242,9 +254,9 @@ module Pubid
|
|
|
242
254
|
(
|
|
243
255
|
(comma >> space >> str("and") >> space >> addendum_code) | # ", and b"
|
|
244
256
|
(space >> str("and") >> space >> addendum_code) | # "and b"
|
|
245
|
-
(comma >> space >> addendum_code).repeat(1,
|
|
246
|
-
(comma >> addendum_code).repeat(1,
|
|
247
|
-
).repeat(0,
|
|
257
|
+
(comma >> space >> addendum_code).repeat(1, 50) | # comma-separated: "a, b, c"
|
|
258
|
+
(comma >> addendum_code).repeat(1, 50) # comma without space: "a,b,c"
|
|
259
|
+
).repeat(0, 20).as(:additional_codes) >> # Multiple groups of codes
|
|
248
260
|
space >>
|
|
249
261
|
(str("to") | str("for")) >> space >>
|
|
250
262
|
((str("ANSI") >> slash >> str("ASHRAE") >> (slash >> letter.repeat(3, 10)).repeat(
|
|
@@ -348,11 +360,11 @@ module Pubid
|
|
|
348
360
|
# Range pattern: "a through z"
|
|
349
361
|
(space >> str("through") >> space >> addendum_code) |
|
|
350
362
|
# Space or comma separated codes (handles typos like "bl bq")
|
|
351
|
-
((space | (comma >> space)) >> addendum_code).repeat(1,
|
|
363
|
+
((space | (comma >> space)) >> addendum_code).repeat(1, 200) |
|
|
352
364
|
# Just match all comma-separated values
|
|
353
|
-
(comma >> space >> addendum_code).repeat(1,
|
|
365
|
+
(comma >> space >> addendum_code).repeat(1, 200) |
|
|
354
366
|
# Or comma without space
|
|
355
|
-
(comma >> addendum_code).repeat(1,
|
|
367
|
+
(comma >> addendum_code).repeat(1, 200)
|
|
356
368
|
).as(:additional_codes).maybe >>
|
|
357
369
|
(space >> additional_copublisher).maybe >>
|
|
358
370
|
addendum_date_suffix.maybe >>
|
|
@@ -486,6 +498,28 @@ module Pubid
|
|
|
486
498
|
errata_suffix_on_addendum.maybe >>
|
|
487
499
|
optional_suffix.repeat(0, 2)
|
|
488
500
|
).as(:publisher_base_addendum) |
|
|
501
|
+
# Format: [ANSI/ASHRAE] Addendum X to/for ASHRAE Standard/Guideline
|
|
502
|
+
# X-YYYY (leading copublisher + publisher-led base)
|
|
503
|
+
(
|
|
504
|
+
(
|
|
505
|
+
(str("ANSI") >> slash >> str("ASHRAE") >> (slash >> letter.repeat(3, 10)).repeat(
|
|
506
|
+
0, 10
|
|
507
|
+
)).as(:copublisher) >>
|
|
508
|
+
space
|
|
509
|
+
).maybe >>
|
|
510
|
+
str("Addendum") >> space >>
|
|
511
|
+
addendum_code >>
|
|
512
|
+
space >>
|
|
513
|
+
(str("to") | str("for")) >> space >>
|
|
514
|
+
publisher.as(:base_publisher) >> space >>
|
|
515
|
+
type.as(:type) >> space >>
|
|
516
|
+
code >>
|
|
517
|
+
(dash >> year_digits.as(:year)).maybe >>
|
|
518
|
+
additional_copublisher.maybe >>
|
|
519
|
+
addendum_date_suffix.maybe >>
|
|
520
|
+
errata_suffix_on_addendum.maybe >>
|
|
521
|
+
optional_suffix.repeat(0, 2)
|
|
522
|
+
).as(:publisher_base_addendum) |
|
|
489
523
|
# Format: ASHRAE Addendum X to ANSI/ASHRAE Standard/Guideline X-YYYY (ASHRAE publisher + copublisher base)
|
|
490
524
|
(
|
|
491
525
|
publisher.as(:publisher) >> space >>
|
|
@@ -99,10 +99,41 @@ module Pubid
|
|
|
99
99
|
|
|
100
100
|
result = id.base.to_s
|
|
101
101
|
result += " Errata"
|
|
102
|
-
|
|
102
|
+
date = long_date(id.date)
|
|
103
|
+
result += " (#{date})" if date
|
|
103
104
|
result
|
|
104
105
|
end
|
|
105
106
|
|
|
107
|
+
# The long date form ASHRAE prints on an errata sheet: "October 10,
|
|
108
|
+
# 2008". The component stores padded numbers, so the day loses its
|
|
109
|
+
# leading zero here.
|
|
110
|
+
#
|
|
111
|
+
# The parser always gives a month and a day together, so only the first
|
|
112
|
+
# two shapes come from a reference string. The others can come from a
|
|
113
|
+
# hand-built identifier or from a hash: a date with no day gives "June
|
|
114
|
+
# 2016", a date with only a year gives "2016", and a month number
|
|
115
|
+
# outside 1-12 falls back to the component's own "2016-13-01" form.
|
|
116
|
+
# They must all print something, because the date reaches `to_hash` and
|
|
117
|
+
# the MR slug, and a surface that drops it silently disagrees with them.
|
|
118
|
+
def long_date(date)
|
|
119
|
+
return nil unless date
|
|
120
|
+
return date.year&.to_s unless date.month
|
|
121
|
+
|
|
122
|
+
month = Builder::MONTH_NAMES[date.month.to_i - 1]
|
|
123
|
+
return date.to_s unless month
|
|
124
|
+
|
|
125
|
+
long_date_with_month(date, month)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# @param month [String] the month name
|
|
129
|
+
# @return [String] the date, with the day when the date carries one
|
|
130
|
+
def long_date_with_month(date, month)
|
|
131
|
+
return "#{month} #{date.year}".strip unless date.day
|
|
132
|
+
|
|
133
|
+
day = "#{month} #{date.day.to_i}"
|
|
134
|
+
date.year ? "#{day}, #{date.year}" : day
|
|
135
|
+
end
|
|
136
|
+
|
|
106
137
|
# Interpretation: "Interpretations for Standard 15.2-2022"
|
|
107
138
|
def render_interpretation(id)
|
|
108
139
|
return id.base.to_s unless id.base
|
|
@@ -5,9 +5,9 @@ module Pubid
|
|
|
5
5
|
class UrnGenerator < Pubid::UrnGenerator::Base
|
|
6
6
|
# A supplement carries no number of its own — only the two
|
|
7
7
|
# single-document leaves declare one — so reach it through #root, which
|
|
8
|
-
# walks `base` to the standard the supplement attaches to.
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# walks `base` to the standard the supplement attaches to. #generate
|
|
9
|
+
# sends a supplement with a `base` to #supplement_urn, so only a
|
|
10
|
+
# supplement without a `base` reaches this fallback.
|
|
11
11
|
def urn_number
|
|
12
12
|
num = identifier.number || identifier.root.number
|
|
13
13
|
return nil if num.nil? || num.to_s.empty?
|
|
@@ -15,6 +15,14 @@ module Pubid
|
|
|
15
15
|
num.to_s
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
# ASHRAE keeps the edition of a document in its own `year` string. The
|
|
19
|
+
# base hook reads the inherited `date` first, which on an Errata holds
|
|
20
|
+
# the date of the erratum — a different thing from the edition year of
|
|
21
|
+
# the standard, and misleading in the year slot. Read `year` only.
|
|
22
|
+
def urn_year
|
|
23
|
+
identifier.year&.to_s
|
|
24
|
+
end
|
|
25
|
+
|
|
18
26
|
def urn_suffix
|
|
19
27
|
identifier.suffix&.to_s&.downcase
|
|
20
28
|
end
|
|
@@ -31,12 +39,17 @@ module Pubid
|
|
|
31
39
|
"copub.#{identifier.copublisher.to_s.downcase}" if identifier.copublisher
|
|
32
40
|
end
|
|
33
41
|
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
"add.#{val}" if val
|
|
42
|
+
def generate
|
|
43
|
+
supplement? ? supplement_urn : document_urn
|
|
37
44
|
end
|
|
38
45
|
|
|
39
|
-
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def supplement?
|
|
49
|
+
identifier.is_a?(SupplementIdentifier) && !identifier.base.nil?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def document_urn
|
|
40
53
|
parts = ["urn", "ashrae"]
|
|
41
54
|
parts << urn_number if urn_number
|
|
42
55
|
parts << urn_year if urn_year
|
|
@@ -48,10 +61,20 @@ module Pubid
|
|
|
48
61
|
|
|
49
62
|
parts[1] = identifier.publisher.to_s.downcase if identifier.publisher
|
|
50
63
|
|
|
51
|
-
parts << urn_addendum if urn_addendum
|
|
52
|
-
|
|
53
64
|
parts.join(":")
|
|
54
65
|
end
|
|
66
|
+
|
|
67
|
+
# A supplement keeps the year, the type and the suffix on its `base`.
|
|
68
|
+
# Its URN is the URN of that base plus one segment that names the
|
|
69
|
+
# supplement ("add.e", "errata.2008-10-10", "interp"). The segment is
|
|
70
|
+
# the marker the MR slug uses, so the two surfaces cannot drift apart.
|
|
71
|
+
# Without this, every supplement of one document had the same bare
|
|
72
|
+
# "urn:ashrae:<number>" (hand-off ashrae-supplement-urn-collapse).
|
|
73
|
+
def supplement_urn
|
|
74
|
+
marker = identifier.mr_supplement_suffix
|
|
75
|
+
[self.class.new(identifier.base).generate, marker]
|
|
76
|
+
.reject { |part| part.nil? || part.empty? }.join(":")
|
|
77
|
+
end
|
|
55
78
|
end
|
|
56
79
|
end
|
|
57
80
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# ASME flavor notes
|
|
2
|
+
|
|
3
|
+
ASME index key and MR slug.
|
|
4
|
+
|
|
5
|
+
These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/asme/` or `spec/pubid/asme/`. The root file keeps the cross-flavor contract that every flavor obeys.
|
|
6
|
+
|
|
7
|
+
## From the root note "AMCA / ASME / ASTM index key (`root.number`): three flavors, three different shapes"
|
|
8
|
+
|
|
9
|
+
**(2) ASME — the whole printed code, not a split.** `Asme::Components::Code` is designator+number, so the IEEE split looks right — but **152 of 731 fixture ids are Boiler and Pressure Vessel Code documents whose entire identity IS the designator** (`BPVC COMPLETE CODE BIND`, `BPVC.CC.BPV`) with no numeric part at all; splitting leaves every one of them keyed `""`. So `number` holds the whole code (`"B18.3"`), declared on the leaf `Identifiers::Standard`. **`Asme::Components::Code` is now unused on the identifier path and there is no `#code` reader**: an earlier draft composed one so the renderer and URN generator would not have to change, but since `number` already holds the whole code that Code never carried a designator and `code.to_s` equalled `number` for all 731 ids — a string wrapped in an object whose only job was to unwrap to the same string. Both readers use `number` directly. **Contrast ASTM, which keeps its composed `#code`**: its renderer reads `code.letter`/`code.dual_m` field-by-field, so there the component carries real structure. The test for whether a derived `#code` earns its place is whether any caller reads a *field* of it rather than just `to_s`.
|
|
10
|
+
|
|
11
|
+
## The trailing year and the BPVC designator (hand-off `asme-bpvc-and-amca-residue`)
|
|
12
|
+
|
|
13
|
+
**A designator with no number lost its year.** The dash branch of `number_part` (`dash >> [0-9A-Z]+`, there for `BTH-1` and `CA-1`) also matched the year in `BPVC.I-2021`, so `-2021` became the number. For a BPVC code the builder then discarded the number, so all 150 BPVC ids had no year and the 2021, 2023 and 2025 editions of one document had the same hash, `to_s`, URN and slug. For `BPE-2012`, `OM-2017` and `PASE-2019` (13 ids) the year stayed inside `number` (`"BPE-2012"`). The `trailing_year` guard in `lib/pubid/asme/parser.rb` refuses that branch for a dash, a 4-digit or draft year, and no further code character. `BTH-1-2020` still parses as `BTH-1`.
|
|
14
|
+
|
|
15
|
+
**Two BPVC render corruptions were in the same builder.** The case sub-code kept its leading dot (`BPVC.CC.BPV..I`, 15 ids), and the builder read the SSC sections one level above where the grammar put them (`BPVC.SSC.`, 2 ids). Every BPVC id now renders as it was written.
|
|
16
|
+
|
|
17
|
+
**`BPVC-CC-BPV` and `BPVC.CC.BPV` are two documents, not two spellings.** The hand-off suggested normalizing one to the other, and a draft of this branch did that. It was reverted: the ASME catalogue (asme.org, "Find codes & standards") lists both as separate documents: `BPVC.CC.BPV` is the code-case book, and `BPVC-CC-BPV` is the designation of each 2019 code-case supplement ("BPVC Code Cases 2019-Boilers and Pressure Vessels, Supplement 7"; the supplement number is only in the title, so pubid cannot tell the supplements apart). Each keeps its own spelling, index key, `to_s` and URN.
|
|
18
|
+
|
|
19
|
+
**The MR slug writes a literal `-` as `--`.** `mr_sanitize` wrote `.` and `-` both as `-`, so `BPVC.CC.BPV-2019` and `BPVC-CC-BPV-2019` shared `asme.bpvc-cc-bpv.2019`. Now the dash form is `asme.bpvc--cc--bpv.2019`. **`_` was the obvious choice and is wrong:** the MR format reserves `_` for supplement layers, and `Pubid::Parsers::MrString` splits on it first. The change moves only the slug, and only for the 113 corpus ids whose code has a literal dash (`BTH-1` → `bth--1`); `B18.3` stays `b18-3`.
|
|
20
|
+
|
|
21
|
+
**The SSC codes of the ASME catalogue all parse.** `BPVC.SSC.XI.II.V.IX` (Sections XI, II, V, IX) and `BPVC.SSC.VIII.XII.II.V.IX` (Sections VIII, XII, II, V, IX) are different documents; the spec table carries every SSC code the catalogue lists.
|
|
22
|
+
|
|
23
|
+
**Measured against a `main` baseline of the 731 fixture ids:** only the 150 BPVC ids and the 13 BPE/OM/PASE ids moved. No id stopped parsing. The `pubid-testsuite` corpus records the old outputs as debt (`tests/asme/_status.yaml` is `clean: false`), so it needs a re-export (hand-off `pubid__pubid-testsuite__asme-amca-reexport`).
|
|
24
|
+
|
|
25
|
+
**Not fixed, pre-existing:** `ASME PTC 1-2015` renders `ASME PTC1-2015` (the space before a PTC number is lost).
|