sec_id 7.1.0 → 8.0.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2ce3f9d548d4530dae973ee4ee4a7cf8e18898aaa36396423efe5ece7dd091f4
4
- data.tar.gz: 5b7950a09cfc4afd59358b230fd659a9f26a1e58e50577ed16267f4763142a7f
3
+ metadata.gz: 070e9688322c11aee220661a15bdc3a6feffeeda6a4d5859620e1e49ef948d0e
4
+ data.tar.gz: 218328185fb31b121fc9fa36cbb2f0d20d7f81542e41e0cab9a453b4e66f366a
5
5
  SHA512:
6
- metadata.gz: 34447319dc249c2fb3b7150cc45cc4a2aa8eed8e11ab16b726d9ee73d7aa2ad01daa2eed846f0cc788fb9f955488b161a91112ea6ec683268af7fa3fdf5267e0
7
- data.tar.gz: 155fd8fe486ea96af91949ddd1adacb04310f50c820a2a5c1d98f1f55075545886c3935adfa5dacf2eedc9975ca7f1879c0e1140a8165dcd60736f24a0bcea94
6
+ metadata.gz: 027535125c743f70d1748956f39530df76f9c537de9b2547d800c55e34ded2d8125c5a3b067edf16016c1aa88eca73ea7cb9567372f1a1875868549969fb620f
7
+ data.tar.gz: 8f3345512540d6a5bfd2edbe436a44948e5c4a45a87a7637462467642b4fcacb590c4f37f675c71d8a38bf8f96f054af9191c2ff84c8798bc0bceff8b8d4a295
data/CHANGELOG.md CHANGED
@@ -8,64 +8,94 @@ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [8.0.0] - 2026-10-30
12
+
13
+ ### Added
14
+
15
+ - Lefthook Git hooks for staged secret scanning, Ruby autocorrection, whitespace, commit messages, tests, types, and documentation coverage.
16
+ - CI checks library CRAP scores with crap4ruby and fresh test coverage, with a maximum score of 10.
17
+
18
+ ### Changed
19
+
20
+ - Reduce detector allocations by sorting candidate types once and stopping parsing at the first valid match. Unsupported input lengths no longer grow the detector cache.
21
+ - Reduce LEI and IBAN checksum allocations by reusing decimal expansions and preserving IBAN numeric characters.
22
+ - Reduce scanner allocations by sorting candidate types once, stopping at the first valid type, and reusing the validated instance.
23
+ - Reduce temporary allocations in ISIN, CUSIP, CEI, and FIGI checksum calculations by processing ASCII bytes directly.
24
+ - Refresh development dependencies, all four lockfiles, and GitHub Actions for frozen CI installs.
25
+
26
+ - **BREAKING:** Minimum Ruby version raised from 3.2 to 3.3 (Ruby 3.2 reached EOL on 2026-03-31)
27
+ - RBS: `SecID::CFI::AttributeSet#to_h` and `#as_json` are now typed `Hash[Symbol, Hash[Symbol, untyped]]` instead of `Hash[untyped, untyped]` (Steep 2.1 tuple inference)
28
+
29
+ ## [7.1.1] - 2026-07-31
30
+
31
+ Documentation and packaging only. No identifier, checksum, or public method changed.
32
+
33
+ This is the first release published from CI through RubyGems trusted publishing, signed with sigstore and pushed with a build provenance attestation.
34
+
35
+ ### Changed
36
+
37
+ - README declares the public API that Semantic Versioning covers, names the `@api private` internals excluded from it, and links where to ask a question and where to report a defect
38
+ - CHANGELOG version headings resolve to comparison links, and 2.0.1 has the entry it never had
39
+ - Gem summary punctuation matches the README tagline
40
+
11
41
  ## [7.1.0] - 2026-07-15
12
42
 
13
43
  ### Added
14
44
 
15
- - `suggest` — a checksum-anchored diagnostic-repair verb that turns the checksum from a gatekeeper into a repair engine. For a structurally-valid but checksum-**failing** identifier, `SecID::<Type>.suggest(str)` and `SecID.suggest(str, types:)` enumerate the plausible single-character human errors — visual/OCR homoglyph substitutions (`O`↔`0`, `I`↔`1`, `5`↔`S`, `8`↔`B`, …) and adjacent transpositions — keep only those that re-validate (`valid?` is the oracle, so no false candidate ever escapes), and return them as confidence-ranked `SecID::Suggestion` value objects reporting *what changed* (edit kind, position, from/to characters, confidence tier). Available for all 9 checksum types (ISIN, CUSIP, SEDOL, FIGI, LEI, IBAN, CEI, DTI, UPI) through one shared `Suggestable` concern. Candidates rank `:high` (homoglyph) then `:medium` (transposition) then a `:checksum` recompute fallback last; there is no `:low` tier (coincidental substitutions are never generated), keeping results small and high-precision. `suggest` never mutates its input and never presents a candidate as an authoritative correction. Non-checksum types (CIK, OCC, WKN, Valoren, CFI, FISN, BIC) have no checksum oracle and are unsupported; `SecID.suggest` silently skips them. Known limitations: the mistyped character must be in the type's charset to be reachable (the vowel-free SEDOL/FIGI/DTI/UPI can't repair an `O`-for-`0`/`I`-for-`1` typo), only a single body error is in scope, and wrong-length input returns `[]`. In a seeded simulation (`benchmark/suggest_precision.rb`), every reachable single-error homoglyph or transposition that yields a checksum-failing identifier is recovered (the correct identifier is always among the returned candidates), the top-ranked body candidate ~89% of the time for homoglyph errors
45
+ - `suggest`, a checksum-anchored diagnostic-repair verb that turns the checksum from a gatekeeper into a repair engine. For a structurally-valid but checksum-**failing** identifier, `SecID::<Type>.suggest(str)` and `SecID.suggest(str, types:)` enumerate the plausible single-character human errors (visual/OCR homoglyph substitutions such as `O`↔`0`, `I`↔`1`, `5`↔`S`, `8`↔`B`, plus adjacent transpositions), keep only those that re-validate (`valid?` is the oracle, so no checksum-invalid candidate ever escapes, though a valid candidate is not necessarily the intended correction), and return them as confidence-ranked `SecID::Suggestion` value objects reporting *what changed* (edit kind, position, from/to characters, confidence tier). Available for all 9 checksum types (ISIN, CUSIP, SEDOL, FIGI, LEI, IBAN, CEI, DTI, UPI) through one shared `Suggestable` concern. Candidates rank `:high` (homoglyph) then `:medium` (transposition) then a `:checksum` recompute fallback last; there is no `:low` tier (coincidental substitutions are never generated), keeping results small and high-precision. `suggest` never mutates its input and never presents a candidate as an authoritative correction. Non-checksum types (CIK, OCC, WKN, Valoren, CFI, FISN, BIC) have no checksum oracle and are unsupported; `SecID.suggest` silently skips them. Known limitations: the mistyped character must be in the type's charset to be reachable (the vowel-free SEDOL/FIGI/DTI/UPI can't repair an `O`-for-`0`/`I`-for-`1` typo), only a single body error is in scope, and wrong-length input returns `[]`. In a seeded simulation (`benchmark/suggest_precision.rb`), every reachable single-error homoglyph or transposition that yields a checksum-failing identifier is recovered (the correct identifier is always among the returned candidates), the top-ranked body candidate ~89% of the time for homoglyph errors
16
46
 
17
47
  ## [7.0.0] - 2026-07-14
18
48
 
19
49
  ### Added
20
50
 
21
- - UPI (ISO 4914, Unique Product Identifier) support via `SecID::UPI` — the gem's 16th identifier type, and the first offline UPI validator in any language. Validates the 12-character code (fixed `QZ` prefix, 9-character body, 1 check character) issued by the ANNA Derivatives Service Bureau for OTC-derivatives reporting (CFTC, EMIR, and other global mandates). The check character is computed fully offline via ISO 7064 hybrid MOD 31,30 over the same 30-symbol alphabet as DTI (digits plus consonants; vowels and `Y` never appear), pinned empirically against DSB-issued vectors — no DSB registry lookup or paywalled ISO 4914 spec required. Like DTI, `checksum`/`calculate_checksum` return a `String` rather than an `Integer`, since UPI check characters can be letters. A UPI shares the 12-character length bucket with ISIN and can double-detect (ISIN ranked first) when its digit check character also satisfies ISIN's Luhn checksum.
51
+ - UPI (ISO 4914, Unique Product Identifier) support via `SecID::UPI`: the gem's 16th identifier type, and the first offline UPI validator in any language. Validates the 12-character code (fixed `QZ` prefix, 9-character body, 1 check character) issued by the ANNA Derivatives Service Bureau for OTC-derivatives reporting (CFTC, EMIR, and other global mandates). The check character is computed fully offline via ISO 7064 hybrid MOD 31,30 over the same 30-symbol alphabet as DTI (digits plus consonants; vowels and `Y` never appear), pinned empirically against DSB-issued vectors, with no DSB registry lookup or paywalled ISO 4914 spec required. Like DTI, `checksum`/`calculate_checksum` return a `String` rather than an `Integer`, since UPI check characters can be letters. A UPI shares the 12-character length bucket with ISIN and can double-detect (ISIN ranked first) when its digit check character also satisfies ISIN's Luhn checksum.
22
52
 
23
53
  ### Changed
24
54
 
25
- - BREAKING: Renamed the check-digit concept to **checksum** across the entire public API, because the old name was wrong on two axes — DTI's and UPI's check value is a `String` (it can be a letter, not a digit), and LEI and IBAN carry a two-character check value. The instance and class methods `check_digit` → `checksum`, `calculate_check_digit` → `calculate_checksum`, and `has_check_digit?` → `has_checksum?`; the error class `SecID::InvalidCheckDigitError` → `SecID::InvalidChecksumError`; the error code `:invalid_check_digit` → `:invalid_checksum` (a **hard flip with no dual emission** — dual would duplicate `errors.details` entries, so update any `errors.details`/`explain`/ActiveModel `details: true` matcher immediately); and the `:check_digit` components key → `:checksum` in `to_h`/`deconstruct_keys` across all nine checkable types. `restore`/`restore!` and the `Checkable` concern name are unchanged. No validation or checksum-arithmetic behavior changed: every identifier that was valid stays valid and every computed value is byte-identical. The old method names, the `InvalidCheckDigitError` constant, and the `:check_digit` components key remain as deprecated bridges through v7 (removed in v8); the error code is the only surface with no bridge. See MIGRATION.md for the full upgrade guide
55
+ - BREAKING: Renamed the check-digit concept to **checksum** across the entire public API, because the old name was wrong on two axes. DTI's and UPI's check value is a `String` (it can be a letter, not a digit), and LEI and IBAN carry a two-character check value. The instance and class methods `check_digit` → `checksum`, `calculate_check_digit` → `calculate_checksum`, and `has_check_digit?` → `has_checksum?`; the error class `SecID::InvalidCheckDigitError` → `SecID::InvalidChecksumError`; the error code `:invalid_check_digit` → `:invalid_checksum` (a **hard flip with no dual emission**: dual would duplicate `errors.details` entries, so update any `errors.details`/`explain`/ActiveModel `details: true` matcher immediately); and the `:check_digit` components key → `:checksum` in `to_h`/`deconstruct_keys` across all nine checkable types. `restore`/`restore!` and the `Checkable` concern name are unchanged. No validation or checksum-arithmetic behavior changed: every identifier that was valid stays valid and every computed value is byte-identical. The old method names, the `InvalidCheckDigitError` constant, and the `:check_digit` components key remain as deprecated bridges through v7 (removed in v8); the error code is the only surface with no bridge. See MIGRATION.md for the full upgrade guide
26
56
 
27
57
  ### Deprecated
28
58
 
29
- - The pre-rename check-digit names, kept as v7 bridges and removed in v8: the `check_digit` / `calculate_check_digit` / `has_check_digit?` methods (instance and class level) — each warns via `Kernel#warn` on every call, visible at Ruby's default verbosity and silenceable with `-W0` / `$VERBOSE = nil` or an app-level `Warning` override; the `SecID::InvalidCheckDigitError` constant, a same-object alias of `InvalidChecksumError` so `rescue` under either name keeps working; and the `:check_digit` key, still present alongside `:checksum` in `components` / `to_h` / `deconstruct_keys`
59
+ - The pre-rename check-digit names, kept as v7 bridges and removed in v8: the `check_digit` / `calculate_check_digit` / `has_check_digit?` methods (instance and class level). Each warns via `Kernel#warn` on every call, visible at Ruby's default verbosity and silenceable with `-W0` / `$VERBOSE = nil` or an app-level `Warning` override; the `SecID::InvalidCheckDigitError` constant, a same-object alias of `InvalidChecksumError` so `rescue` under either name keeps working; and the `:check_digit` key, still present alongside `:checksum` in `components` / `to_h` / `deconstruct_keys`
30
60
 
31
61
  ## [6.1.0] - 2026-07-10
32
62
 
33
63
  ### Added
34
64
 
35
- - `SecID::Base#deconstruct_keys` — every identifier now destructures in `case/in` patterns, exposing the same parsed fields `#to_h` reports under `:components`. The `keys` argument is ignored, no new keys are introduced, and validity is not part of the protocol: `SecID.parse` returning `nil` remains the validity guard, and an instance built from unparseable input binds `nil` for each component. No `deconstruct` (array-pattern) method is defined
36
- - `SecID::Base.type_key` — the memoized class-level registry symbol for an identifier type, joining the existing `short_name` / `full_name` / `id_length` metadata surface. It round-trips through the registry: `SecID[SecID::ISIN.type_key] == SecID::ISIN`. It is now the single authority for that symbol; the registry, `#to_h`, `SecID.explain`, the ActiveModel validator, the detector, and the scanner all read it instead of each deriving it from the class name
65
+ - `SecID::Base#deconstruct_keys`: every identifier now destructures in `case/in` patterns, exposing the same parsed fields `#to_h` reports under `:components`. The `keys` argument is ignored, no new keys are introduced, and validity is not part of the protocol: `SecID.parse` returning `nil` remains the validity guard, and an instance built from unparseable input binds `nil` for each component. No `deconstruct` (array-pattern) method is defined
66
+ - `SecID::Base.type_key`: the memoized class-level registry symbol for an identifier type, joining the existing `short_name` / `full_name` / `id_length` metadata surface. It round-trips through the registry: `SecID[SecID::ISIN.type_key] == SecID::ISIN`. It is now the single authority for that symbol; the registry, `#to_h`, `SecID.explain`, the ActiveModel validator, the detector, and the scanner all read it instead of each deriving it from the class name
37
67
 
38
68
  ## [6.0.0] - 2026-07-08
39
69
 
40
70
  ### Added
41
71
 
42
- - Hand-written RBS type signatures under `sig/`, shipped in the gem, so consumers running Steep or an RBS-aware editor resolve sec_id's types on install. The core library is checked by Steep in strict mode with zero errors and verified at runtime against the specs via RBS::Test. New dev/test tooling (`rbs` and `steep`, `require: false` — the gem stays zero-runtime-dependency) and rake tasks: `rake steep` (strict type check), `rake steep:coverage` (untyped-call gate), `rake rbs:test` (runtime signature verification), `rake rbs` (validate signatures, in the default task), and `rake sig:cfi` (regenerate the CFI dynamic-method signatures from `SecID::CFI::Tables`, guarded by a drift spec). CI gains a `types` job on Ruby 4.0. The optional ActiveModel adapter and Railtie are excluded from the typed scope
43
- - `SecID::CFI#decode` returns a frozen `SecID::CFI::Classification` value object for any valid CFI (`nil` for invalid). Its category, group, and each attribute are `Field` objects carrying the CFI letter (`#code`), semantic symbol (`#name`), and authoritative ISO 10962 label (`#label`), plus `<name>?` predicates scoped to the field's own domain — so `decode.category.equity?` and `decode.attributes.voting_right.voting?` answer, while an out-of-domain predicate (e.g. `category.voting?`) raises `NoMethodError`. `#attributes` is an `Enumerable` of the attribute fields keyed by each position's group meaning (`attributes.voting_right`, nil-safe `attributes[:form]`). Ships a human-readable `#to_s` and `#to_h`/`#as_json` for serialization. `X` decodes to `:not_applicable`; pure-N/A positions are omitted
44
- - `SecID::CFI` now validates the full ISO 10962:2021 attribute matrix — positions 3–6 are checked per-group, `Strategies` (`K`) codes require `XXXX`, pure-N/A positions accept only `X`, and the `ED` (depositary receipts on equities) cross-position rule is enforced. A new `:invalid_attribute` error code (mapped to `InvalidStructureError`) names the offending positions and group
45
- - Opt-in ActiveModel / Rails validator (`require 'sec_id/active_model'`), registered as `sec_id`, for declarative validation: `validates :isin, sec_id: { type: :isin }`. Validates a single type (`type:`), an allowlist (`types:`), or any supported type (`sec_id: true`); a bad type raises `ArgumentError` at class-load. Opt-in `normalize: true` accepts separatored input and rewrites the attribute to canonical form on success; opt-in `details: true` surfaces the specific failure reason. Auto-activates in Rails via a Railtie (no `require:`/initializer needed) and adds **no runtime dependency** — nothing on the default `require 'sec_id'` path loads Rails or ActiveModel. Tested across Rails 7.2, 8.0, 8.1, and main
46
- - BIC / SWIFT code (ISO 9362) support via `SecID::BIC` — validate, normalize, parse, detect, extract, and generate 8- or 11-character Business Identifier Codes. Exposes `bank_code`, `country_code`, `location_code`, and `branch_code` (`nil` for a BIC8) components. Validates the embedded country code against a frozen ISO 3166-1 / SWIFT-recognized set (`SecID::BIC.countries`), raising `InvalidStructureError` (`:invalid_country`) for unrecognized codes. Validation confirms structure and a real country code only — it does not verify registration in the licensed SWIFT registry
47
- - DTI (ISO 24165, Digital Token Identifier) support via `SecID::DTI` — the gem's 15th identifier type, and the first offline DTI validator in any language. Validates the ISO 7064 hybrid MOD 31,30 check character over the 30-symbol DTI alphabet (digits plus consonants; vowels and `Y` never appear), identified empirically from public registry data (1,595 of 1,596 snapshot codes plus 2 post-snapshot registrations). Bitcoin's hand-assigned code (`4H95J0R2X`, which fails the algorithm) is honored via a frozen exception map consulted by `valid?`, `restore`, and `check_digit` alike. Unlike every other check-digit type in the gem, `check_digit`/`calculate_check_digit` return a `String` rather than an `Integer`, since DTI check characters can be letters
48
- - `.generate` on all 15 identifier types plus a central `SecID.generate(:type)` dispatcher for producing syntactically valid identifiers (with correct check digits where applicable) as test fixtures — accepts an optional `random:` keyword (a Ruby `Random`) for reproducible output. Generated values are valid in format only and are not real, registered securities
49
- - 100% YARD documentation coverage of the public API, enforced in CI via `rake yard:stats`. The gem now ships a `.yardopts` so `rubydoc.info` renders the public API cleanly (private internals hidden, README/CHANGELOG/LICENSE as pages) and exposes `documentation_uri` metadata (a Documentation link on the RubyGems page). Adds a `yard` dev/test dependency (`require: false` — the gem stays zero-runtime-dependency) and a `rake yard` task for local HTML docs
72
+ - Hand-written RBS type signatures under `sig/`, shipped in the gem, so consumers running Steep or an RBS-aware editor resolve sec_id's types on install. The core library is checked by Steep in strict mode with zero errors and verified at runtime against the specs via RBS::Test. New dev/test tooling (`rbs` and `steep`, `require: false`; the gem stays zero-runtime-dependency) and rake tasks: `rake steep` (strict type check), `rake steep:coverage` (untyped-call gate), `rake rbs:test` (runtime signature verification), `rake rbs` (validate signatures, in the default task), and `rake sig:cfi` (regenerate the CFI dynamic-method signatures from `SecID::CFI::Tables`, guarded by a drift spec). CI gains a `types` job on Ruby 4.0. The optional ActiveModel adapter and Railtie are excluded from the typed scope
73
+ - `SecID::CFI#decode` returns a frozen `SecID::CFI::Classification` value object for any valid CFI (`nil` for invalid). Its category, group, and each attribute are `Field` objects carrying the CFI letter (`#code`), semantic symbol (`#name`), and authoritative ISO 10962 label (`#label`), plus `<name>?` predicates scoped to the field's own domain, so `decode.category.equity?` and `decode.attributes.voting_right.voting?` answer, while an out-of-domain predicate (e.g. `category.voting?`) raises `NoMethodError`. `#attributes` is an `Enumerable` of the attribute fields keyed by each position's group meaning (`attributes.voting_right`, nil-safe `attributes[:form]`). Ships a human-readable `#to_s` and `#to_h`/`#as_json` for serialization. `X` decodes to `:not_applicable`; pure-N/A positions are omitted
74
+ - `SecID::CFI` now validates the full ISO 10962:2021 attribute matrix: positions 3 to 6 are checked per-group, `Strategies` (`K`) codes require `XXXX`, pure-N/A positions accept only `X`, and the `ED` (depositary receipts on equities) cross-position rule is enforced. A new `:invalid_attribute` error code (mapped to `InvalidStructureError`) names the offending positions and group
75
+ - Opt-in ActiveModel / Rails validator (`require 'sec_id/active_model'`), registered as `sec_id`, for declarative validation: `validates :isin, sec_id: { type: :isin }`. Validates a single type (`type:`), an allowlist (`types:`), or any supported type (`sec_id: true`); a bad type raises `ArgumentError` at class-load. Opt-in `normalize: true` accepts separatored input and rewrites the attribute to canonical form on success; opt-in `details: true` surfaces the specific failure reason. Auto-activates in Rails via a Railtie (no `require:`/initializer needed) and adds **no runtime dependency**: nothing on the default `require 'sec_id'` path loads Rails or ActiveModel. Tested across Rails 7.2, 8.0, 8.1, and main
76
+ - BIC / SWIFT code (ISO 9362) support via `SecID::BIC`: validate, normalize, parse, detect, extract, and generate 8- or 11-character Business Identifier Codes. Exposes `bank_code`, `country_code`, `location_code`, and `branch_code` (`nil` for a BIC8) components. Validates the embedded country code against a frozen ISO 3166-1 / SWIFT-recognized set (`SecID::BIC.countries`), raising `InvalidStructureError` (`:invalid_country`) for unrecognized codes. Validation confirms structure and a real country code only. It does not verify registration in the licensed SWIFT registry
77
+ - DTI (ISO 24165, Digital Token Identifier) support via `SecID::DTI`: the gem's 15th identifier type, and the first offline DTI validator in any language. Validates the ISO 7064 hybrid MOD 31,30 check character over the 30-symbol DTI alphabet (digits plus consonants; vowels and `Y` never appear), identified empirically from public registry data (1,595 of 1,596 snapshot codes plus 2 post-snapshot registrations). Bitcoin's hand-assigned code (`4H95J0R2X`, which fails the algorithm) is honored via a frozen exception map consulted by `valid?`, `restore`, and `check_digit` alike. Unlike every other check-digit type in the gem, `check_digit`/`calculate_check_digit` return a `String` rather than an `Integer`, since DTI check characters can be letters
78
+ - `.generate` on all 15 identifier types plus a central `SecID.generate(:type)` dispatcher for producing syntactically valid identifiers (with correct check digits where applicable) as test fixtures. Accepts an optional `random:` keyword (a Ruby `Random`) for reproducible output. Generated values are valid in format only and are not real, registered securities
79
+ - 100% YARD documentation coverage of the public API, enforced in CI via `rake yard:stats`. The gem now ships a `.yardopts` so `rubydoc.info` renders the public API cleanly (private internals hidden, README/CHANGELOG/LICENSE as pages) and exposes `documentation_uri` metadata (a Documentation link on the RubyGems page). Adds a `yard` dev/test dependency (`require: false`; the gem stays zero-runtime-dependency) and a `rake yard` task for local HTML docs
50
80
 
51
81
  ### Changed
52
82
 
53
83
  - Internal `@api private` IBAN country-data module dissolved into the `SecID::IBAN` class, so constant names match their file paths per Ruby convention: `SecID::IBANCountryRules::COUNTRY_RULES` / `LENGTH_ONLY_COUNTRIES` are now `SecID::IBAN::COUNTRY_RULES` / `SecID::IBAN::LENGTH_ONLY_COUNTRIES`
54
- - `SecID.valid?` and `SecID.parse` are faster and allocate less — `SecID.valid?` short-circuits on the first matching type instead of running a full detect-and-sort, and `SecID.parse` reuses the instance built during detection instead of instantiating the matched type a second time
55
- - **BREAKING:** `SecID::CFI.valid?` is now strict at the attribute level for all 14 categories (including the derivative categories `S`, `H`, `J`) — codes with attribute letters outside the ISO 10962:2021 tables, previously accepted, are now invalid (e.g. `CFI.valid?('ESZZZZ')` returns `false`). There is no leniency option, matching every other identifier type in the gem. The ActiveModel validator inherits this strictness automatically
56
- - **BREAKING:** `SecID::CFI` group tables corrected to ISO 10962:2021 — six categories carried wrong group letters. Non-listed options (`H`) are now classified by underlying (`HR` Rates, `HT` Commodities, `HE` Equity, `HC` Credit, `HF` FX, `HM` Others) instead of the copied listed-option call/put shape; `D` gains `DE` (structured products without capital protection) and corrects `DG`/`DA`/`DN` (dropping the invented `municipal_notes`); `L` becomes `LL`/`LR`/`LS`, `T` becomes `TC`/`TT`/`TR`/`TI`/…, and the phantom `FM`/`IM`/`JM`/`LM` groups are removed. Group symbols renamed accordingly (e.g. `LS` → `:securities_lending`, `TI` → `:indices`, `MM` → `:other_assets`). Real `H`-category codes that were rejected now validate; codes relying on the old wrong letters no longer do
84
+ - `SecID.valid?` and `SecID.parse` are faster and allocate less. `SecID.valid?` short-circuits on the first matching type instead of running a full detect-and-sort, and `SecID.parse` reuses the instance built during detection instead of instantiating the matched type a second time
85
+ - **BREAKING:** `SecID::CFI.valid?` is now strict at the attribute level for all 14 categories (including the derivative categories `S`, `H`, `J`). Codes with attribute letters outside the ISO 10962:2021 tables, previously accepted, are now invalid (e.g. `CFI.valid?('ESZZZZ')` returns `false`). There is no leniency option, matching every other identifier type in the gem. The ActiveModel validator inherits this strictness automatically
86
+ - **BREAKING:** `SecID::CFI` group tables corrected to ISO 10962:2021: six categories carried wrong group letters. Non-listed options (`H`) are now classified by underlying (`HR` Rates, `HT` Commodities, `HE` Equity, `HC` Credit, `HF` FX, `HM` Others) instead of the copied listed-option call/put shape; `D` gains `DE` (structured products without capital protection) and corrects `DG`/`DA`/`DN` (dropping the invented `municipal_notes`); `L` becomes `LL`/`LR`/`LS`, `T` becomes `TC`/`TT`/`TR`/`TI`/…, and the phantom `FM`/`IM`/`JM`/`LM` groups are removed. Group symbols renamed accordingly (e.g. `LS` → `:securities_lending`, `TI` → `:indices`, `MM` → `:other_assets`). Real `H`-category codes that were rejected now validate; codes relying on the old wrong letters no longer do
57
87
 
58
88
  ### Removed
59
89
 
60
- - **BREAKING:** the 12 hardcoded equity predicate helpers on `SecID::CFI` (`equity?`, `voting?`, `non_voting?`, `restricted_voting?`, `enhanced_voting?`, `restrictions?`, `no_restrictions?`, `fully_paid?`, `nil_paid?`, `partly_paid?`, `bearer?`, `registered?`). They were category-wide and semantically wrong for non-equity groups. Migration: use `cfi.decode` and its scoped field predicates — `cfi.voting?` → `cfi.decode.attributes.voting_right.voting?` — which answer only on the field whose ISO domain defines the concept. Two names do not carry over name-for-name: migrate `cfi.equity?` to `cfi.decode.category.equity?` (or `cfi.category == :equity`, unchanged), and `cfi.no_restrictions?` to `cfi.decode.attributes.ownership_restrictions.free_of_restrictions?` (the ISO value name)
90
+ - **BREAKING:** the 12 hardcoded equity predicate helpers on `SecID::CFI` (`equity?`, `voting?`, `non_voting?`, `restricted_voting?`, `enhanced_voting?`, `restrictions?`, `no_restrictions?`, `fully_paid?`, `nil_paid?`, `partly_paid?`, `bearer?`, `registered?`). They were category-wide and semantically wrong for non-equity groups. Migration: use `cfi.decode` and its scoped field predicates, so `cfi.voting?` becomes `cfi.decode.attributes.voting_right.voting?`, which answers only on the field whose ISO domain defines the concept. Two names do not carry over name-for-name: migrate `cfi.equity?` to `cfi.decode.category.equity?` (or `cfi.category == :equity`, unchanged), and `cfi.no_restrictions?` to `cfi.decode.attributes.ownership_restrictions.free_of_restrictions?` (the ISO value name)
61
91
 
62
92
  ## [5.2.0] - 2026-02-24
63
93
 
64
94
  ### Added
65
95
 
66
- - `SecID.scan` and `SecID.extract` methods for finding identifiers in freeform text — returns `Scanner::Match` objects (`Data.define(:type, :raw, :range, :identifier)`) with the validated identifier instance; supports `types:` filtering, hyphenated identifiers, and compound patterns (OCC with spaces, FISN with slashes)
67
- - `SecID.explain` method for debugging identifier detection — returns per-type validation results showing exactly why each type matched or rejected the input
68
- - `on_ambiguous:` option for `SecID.parse` and `SecID.parse!` — `:first` (default, existing behavior), `:raise` (raises `AmbiguousMatchError`), `:all` (returns array of all matching instances)
96
+ - `SecID.scan` and `SecID.extract` methods for finding identifiers in freeform text. Returns `Scanner::Match` objects (`Data.define(:type, :raw, :range, :identifier)`) with the validated identifier instance; supports `types:` filtering, hyphenated identifiers, and compound patterns (OCC with spaces, FISN with slashes)
97
+ - `SecID.explain` method for debugging identifier detection. Returns per-type validation results showing exactly why each type matched or rejected the input
98
+ - `on_ambiguous:` option for `SecID.parse` and `SecID.parse!`: `:first` (default, existing behavior), `:raise` (raises `AmbiguousMatchError`), `:all` (returns array of all matching instances)
69
99
  - `SecID::AmbiguousMatchError` exception class for ambiguous identifier detection
70
100
  - `#as_json` method on all identifier types (delegates to `#to_h`) and on `Errors` (delegates to `#details`) for JSON serialization compatibility
71
101
  - `SecID::IBAN.supported_countries` class method returning sorted array of all supported country codes
@@ -77,9 +107,9 @@ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
77
107
 
78
108
  ### Added
79
109
 
80
- - `#==`, `#eql?`, and `#hash` methods on all identifier types — two instances of the same type with the same normalized form are equal and usable as Hash keys / in Sets
81
- - `#to_h` method on all identifier types for consistent hash serialization — returns `{ type:, full_id:, normalized:, valid:, components: }` with type-specific component hashes (e.g. ISIN: `country_code`, `nsin`, `check_digit`)
82
- - `#to_pretty_s` and `.to_pretty_s` display formatting methods on all identifier types, returning a human-readable string or `nil` for invalid input — with type-specific formats for IBAN (4-char groups), LEI (4-char groups), ISIN (CC + NSIN + CD), CUSIP (cusip6 + issue + CD), FIGI (prefix+G + random + CD), OCC (space-separated components), and Valoren (thousands grouping)
110
+ - `#==`, `#eql?`, and `#hash` methods on all identifier types. Two instances of the same type with the same normalized form are equal and usable as Hash keys / in Sets
111
+ - `#to_h` method on all identifier types for consistent hash serialization. Returns `{ type:, full_id:, normalized:, valid:, components: }` with type-specific component hashes (e.g. ISIN: `country_code`, `nsin`, `check_digit`)
112
+ - `#to_pretty_s` and `.to_pretty_s` display formatting methods on all identifier types, returning a human-readable string or `nil` for invalid input, with type-specific formats for IBAN (4-char groups), LEI (4-char groups), ISIN (CC + NSIN + CD), CUSIP (cusip6 + issue + CD), FIGI (prefix+G + random + CD), OCC (space-separated components), and Valoren (thousands grouping)
83
113
  - Lookup service integration guides and runnable examples for OpenFIGI, SEC EDGAR, GLEIF, and Eurex APIs (`docs/guides/`, `examples/`)
84
114
  - GitHub community standards files: Code of Conduct, Contributing guide, Security policy, issue templates, and PR template
85
115
 
@@ -117,15 +147,15 @@ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
117
147
 
118
148
  ### Removed
119
149
 
120
- - Class-level `.normalize!` on CIK, OCC, and Valoren — replaced by `.normalize`
150
+ - Class-level `.normalize!` on CIK, OCC, and Valoren, replaced by `.normalize`
121
151
  - `upcase` keyword parameter from `Base#parse`
122
152
  - `#valid_format?` instance method (now private) and `.valid_format?` class method
123
- - `OCC#full_symbol` method — use `#full_id` instead
153
+ - `OCC#full_symbol` method. Use `#full_id` instead
124
154
 
125
155
  ### Fixed
126
156
 
127
- - `to_str` now always returns the same value as `to_s` across all identifier types — previously LEI, IBAN, and Checkable identifiers could return divergent strings due to Ruby `alias` resolving to the parent class method
128
- - OCC `#date` memoization for invalid dates — previously re-attempted parsing on every call instead of caching `nil`
157
+ - `to_str` now always returns the same value as `to_s` across all identifier types. Previously LEI, IBAN, and Checkable identifiers could return divergent strings due to Ruby `alias` resolving to the parent class method
158
+ - OCC `#date` memoization for invalid dates. Previously it re-attempted parsing on every call instead of caching `nil`
129
159
  - LEI `restore` and `to_s` now correctly pad single-digit check digits to 2 characters
130
160
  - `Valoren#to_isin` no longer mutates the source instance
131
161
 
@@ -159,7 +189,7 @@ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
159
189
  ### Fixed
160
190
 
161
191
  - Allow Crown Dependencies (GG, IM, JE) and Overseas Territories (FK) in SEDOL/ISIN conversions ([@wtn](https://github.com/wtn), [#117](https://github.com/svyatov/sec_id/pull/117))
162
- - Removed BR (Brazil) from CGS country codes — Brazil never used CINS numbers and Brazilian ISINs cannot be converted to CUSIP ([@wtn](https://github.com/wtn), [#110](https://github.com/svyatov/sec_id/pull/110))
192
+ - Removed BR (Brazil) from CGS country codes. Brazil never used CINS numbers and Brazilian ISINs cannot be converted to CUSIP ([@wtn](https://github.com/wtn), [#110](https://github.com/svyatov/sec_id/pull/110))
163
193
 
164
194
  ## [4.3.0] - 2025-01-13
165
195
 
@@ -174,7 +204,7 @@ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
174
204
  - Refactored CIK and OCC to inherit from Base class with `has_check_digit?` hook for cleaner architecture
175
205
  - Added `Normalizable` module for consistent `normalize!` class method across identifiers
176
206
  - Added `validate_format_for_calculation!` helper method to Base class to reduce code duplication
177
- - Added comprehensive YARD documentation to all classes (public and private methods)
207
+ - Added YARD documentation to all classes (public and private methods)
178
208
  - Applied Stepdown Rule to method ordering throughout codebase
179
209
  - Created shared RSpec examples for edge cases (nil, empty, whitespace inputs)
180
210
  - Created shared RSpec examples for check-digit and normalization identifiers
@@ -232,6 +262,10 @@ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
232
262
  - TravisCI config updated: dropped Ruby 2.3 and 2.4, added Ruby 2.7
233
263
  - Rubocop's Ruby target version changed to 2.5
234
264
 
265
+ ## [2.0.1] - 2019-02-04
266
+
267
+ Gem metadata only: corrected the gemspec description and README wording. No library change.
268
+
235
269
  ## [2.0.0] - 2019-02-03
236
270
 
237
271
  ### Added
@@ -268,3 +302,25 @@ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
268
302
  ### Added
269
303
 
270
304
  - ISIN numbers support: `SecID::ISIN`
305
+
306
+ [Unreleased]: https://github.com/svyatov/sec_id/compare/v8.0.0...HEAD
307
+ [8.0.0]: https://github.com/svyatov/sec_id/compare/v7.1.1...v8.0.0
308
+ [7.1.1]: https://github.com/svyatov/sec_id/compare/v7.1.0...v7.1.1
309
+ [7.1.0]: https://github.com/svyatov/sec_id/compare/v7.0.0...v7.1.0
310
+ [7.0.0]: https://github.com/svyatov/sec_id/compare/v6.1.0...v7.0.0
311
+ [6.1.0]: https://github.com/svyatov/sec_id/compare/v6.0.0...v6.1.0
312
+ [6.0.0]: https://github.com/svyatov/sec_id/compare/v5.2.0...v6.0.0
313
+ [5.2.0]: https://github.com/svyatov/sec_id/compare/v5.1.0...v5.2.0
314
+ [5.1.0]: https://github.com/svyatov/sec_id/compare/v5.0.0...v5.1.0
315
+ [5.0.0]: https://github.com/svyatov/sec_id/compare/v4.4.1...v5.0.0
316
+ [4.4.1]: https://github.com/svyatov/sec_id/compare/v4.4.0...v4.4.1
317
+ [4.4.0]: https://github.com/svyatov/sec_id/compare/v4.3.0...v4.4.0
318
+ [4.3.0]: https://github.com/svyatov/sec_id/compare/v4.2.0...v4.3.0
319
+ [4.2.0]: https://github.com/svyatov/sec_id/compare/v4.1.0...v4.2.0
320
+ [4.1.0]: https://github.com/svyatov/sec_id/compare/v4.0.0...v4.1.0
321
+ [4.0.0]: https://github.com/svyatov/sec_id/compare/v3.0.0...v4.0.0
322
+ [3.0.0]: https://github.com/svyatov/sec_id/compare/v2.0.1...v3.0.0
323
+ [2.0.1]: https://github.com/svyatov/sec_id/compare/v2.0.0...v2.0.1
324
+ [2.0.0]: https://github.com/svyatov/sec_id/compare/v1.1.0...v2.0.0
325
+ [1.1.0]: https://github.com/svyatov/sec_id/compare/v1.0.0...v1.1.0
326
+ [1.0.0]: https://github.com/svyatov/sec_id/releases/tag/v1.0.0