active_sanction 1.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.
Files changed (143) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +538 -0
  3. data/CODE_OF_CONDUCT.md +74 -0
  4. data/CONTRIBUTING.md +312 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +616 -0
  7. data/SECURITY.md +97 -0
  8. data/active_sanction.gemspec +98 -0
  9. data/docs/adding_a_source.md +1083 -0
  10. data/docs/api_stability.md +357 -0
  11. data/docs/bundle_format.md +407 -0
  12. data/lib/active_sanction/address.rb +170 -0
  13. data/lib/active_sanction/client.rb +341 -0
  14. data/lib/active_sanction/configuration.rb +721 -0
  15. data/lib/active_sanction/countries.txt +292 -0
  16. data/lib/active_sanction/country.rb +164 -0
  17. data/lib/active_sanction/deprecation.rb +168 -0
  18. data/lib/active_sanction/diff/change.rb +208 -0
  19. data/lib/active_sanction/diff.rb +348 -0
  20. data/lib/active_sanction/doctor/checkup.rb +356 -0
  21. data/lib/active_sanction/doctor/diagnosis.rb +245 -0
  22. data/lib/active_sanction/doctor/finding.rb +183 -0
  23. data/lib/active_sanction/doctor/profile.rb +384 -0
  24. data/lib/active_sanction/doctor/report.rb +227 -0
  25. data/lib/active_sanction/doctor.rb +347 -0
  26. data/lib/active_sanction/entity.rb +293 -0
  27. data/lib/active_sanction/error.rb +332 -0
  28. data/lib/active_sanction/fetcher/result.rb +116 -0
  29. data/lib/active_sanction/fetcher.rb +242 -0
  30. data/lib/active_sanction/http_client/errors.rb +94 -0
  31. data/lib/active_sanction/http_client/response.rb +129 -0
  32. data/lib/active_sanction/http_client.rb +324 -0
  33. data/lib/active_sanction/identifier.rb +212 -0
  34. data/lib/active_sanction/index/builder.rb +89 -0
  35. data/lib/active_sanction/index/candidate.rb +63 -0
  36. data/lib/active_sanction/index/entry.rb +66 -0
  37. data/lib/active_sanction/index/features.rb +112 -0
  38. data/lib/active_sanction/index.rb +381 -0
  39. data/lib/active_sanction/match_result.rb +345 -0
  40. data/lib/active_sanction/matcher.rb +388 -0
  41. data/lib/active_sanction/name.rb +183 -0
  42. data/lib/active_sanction/normalizer/cache.rb +82 -0
  43. data/lib/active_sanction/normalizer/dictionaries/honorifics.txt +30 -0
  44. data/lib/active_sanction/normalizer/dictionaries/legal_forms.txt +72 -0
  45. data/lib/active_sanction/normalizer/dictionaries/organization_stopwords.txt +20 -0
  46. data/lib/active_sanction/normalizer/dictionaries/particles.txt +35 -0
  47. data/lib/active_sanction/normalizer/dictionary/stoplist.rb +121 -0
  48. data/lib/active_sanction/normalizer/dictionary.rb +243 -0
  49. data/lib/active_sanction/normalizer/form.rb +279 -0
  50. data/lib/active_sanction/normalizer.rb +137 -0
  51. data/lib/active_sanction/parsers/column_shape.rb +274 -0
  52. data/lib/active_sanction/parsers/delimited_table/reader.rb +194 -0
  53. data/lib/active_sanction/parsers/delimited_table/row.rb +81 -0
  54. data/lib/active_sanction/parsers/delimited_table.rb +151 -0
  55. data/lib/active_sanction/parsers/format.rb +107 -0
  56. data/lib/active_sanction/parsers/join.rb +149 -0
  57. data/lib/active_sanction/parsers/spreadsheet/archive.rb +258 -0
  58. data/lib/active_sanction/parsers/spreadsheet/reader.rb +213 -0
  59. data/lib/active_sanction/parsers/spreadsheet/row.rb +88 -0
  60. data/lib/active_sanction/parsers/spreadsheet/workbook.rb +314 -0
  61. data/lib/active_sanction/parsers/spreadsheet.rb +168 -0
  62. data/lib/active_sanction/parsers/xml_records/backends/nokogiri.rb +134 -0
  63. data/lib/active_sanction/parsers/xml_records/backends/rexml.rb +157 -0
  64. data/lib/active_sanction/parsers/xml_records/backends.rb +103 -0
  65. data/lib/active_sanction/parsers/xml_records/builder.rb +90 -0
  66. data/lib/active_sanction/parsers/xml_records/reader.rb +134 -0
  67. data/lib/active_sanction/parsers/xml_records/record.rb +174 -0
  68. data/lib/active_sanction/parsers/xml_records.rb +140 -0
  69. data/lib/active_sanction/parsers.rb +94 -0
  70. data/lib/active_sanction/partial_date/parser.rb +124 -0
  71. data/lib/active_sanction/partial_date.rb +303 -0
  72. data/lib/active_sanction/payload_cache/checksum.rb +66 -0
  73. data/lib/active_sanction/payload_cache/entry.rb +304 -0
  74. data/lib/active_sanction/payload_cache.rb +366 -0
  75. data/lib/active_sanction/phonetics/double_metaphone.rb +662 -0
  76. data/lib/active_sanction/phonetics.rb +40 -0
  77. data/lib/active_sanction/query.rb +290 -0
  78. data/lib/active_sanction/rescreen/alert.rb +314 -0
  79. data/lib/active_sanction/rescreen.rb +332 -0
  80. data/lib/active_sanction/scorer/adjustments.rb +241 -0
  81. data/lib/active_sanction/scorer/name_score.rb +279 -0
  82. data/lib/active_sanction/scorer/reason.rb +136 -0
  83. data/lib/active_sanction/scorer/result.rb +129 -0
  84. data/lib/active_sanction/scorer/subject.rb +235 -0
  85. data/lib/active_sanction/scorer/weights.rb +306 -0
  86. data/lib/active_sanction/scorer.rb +303 -0
  87. data/lib/active_sanction/similarity/jaro_winkler.rb +210 -0
  88. data/lib/active_sanction/similarity/levenshtein.rb +179 -0
  89. data/lib/active_sanction/similarity/token_set.rb +159 -0
  90. data/lib/active_sanction/similarity/token_sort.rb +120 -0
  91. data/lib/active_sanction/similarity.rb +183 -0
  92. data/lib/active_sanction/snapshot/bundle/header.rb +302 -0
  93. data/lib/active_sanction/snapshot/bundle/payload.rb +185 -0
  94. data/lib/active_sanction/snapshot/bundle/signature.rb +201 -0
  95. data/lib/active_sanction/snapshot/bundle.rb +381 -0
  96. data/lib/active_sanction/snapshot.rb +305 -0
  97. data/lib/active_sanction/sources/australia_dfat/published_date.rb +166 -0
  98. data/lib/active_sanction/sources/australia_dfat/record.rb +319 -0
  99. data/lib/active_sanction/sources/australia_dfat.rb +291 -0
  100. data/lib/active_sanction/sources/base.rb +295 -0
  101. data/lib/active_sanction/sources/canada_sema/record.rb +282 -0
  102. data/lib/active_sanction/sources/canada_sema/source_ref.rb +89 -0
  103. data/lib/active_sanction/sources/canada_sema.rb +153 -0
  104. data/lib/active_sanction/sources/definition.rb +360 -0
  105. data/lib/active_sanction/sources/eu_fsf/record.rb +467 -0
  106. data/lib/active_sanction/sources/eu_fsf.rb +203 -0
  107. data/lib/active_sanction/sources/ofac/record.rb +251 -0
  108. data/lib/active_sanction/sources/ofac/remarks_parser/coverage.rb +120 -0
  109. data/lib/active_sanction/sources/ofac/remarks_parser/vocabulary.rb +121 -0
  110. data/lib/active_sanction/sources/ofac/remarks_parser.rb +312 -0
  111. data/lib/active_sanction/sources/ofac.rb +248 -0
  112. data/lib/active_sanction/sources/ofac_consolidated/record.rb +37 -0
  113. data/lib/active_sanction/sources/ofac_consolidated.rb +203 -0
  114. data/lib/active_sanction/sources/ofac_sdn.rb +43 -0
  115. data/lib/active_sanction/sources/remarks.rb +84 -0
  116. data/lib/active_sanction/sources/uk_sanctions_list/published_date.rb +104 -0
  117. data/lib/active_sanction/sources/uk_sanctions_list/record.rb +412 -0
  118. data/lib/active_sanction/sources/uk_sanctions_list.rb +214 -0
  119. data/lib/active_sanction/sources/un_consolidated/record.rb +279 -0
  120. data/lib/active_sanction/sources/un_consolidated.rb +140 -0
  121. data/lib/active_sanction/sources.rb +196 -0
  122. data/lib/active_sanction/storage/active_record/reader.rb +124 -0
  123. data/lib/active_sanction/storage/active_record/row.rb +175 -0
  124. data/lib/active_sanction/storage/active_record/writer.rb +141 -0
  125. data/lib/active_sanction/storage/active_record.rb +277 -0
  126. data/lib/active_sanction/storage/base.rb +216 -0
  127. data/lib/active_sanction/storage/file_system.rb +373 -0
  128. data/lib/active_sanction/storage/memory.rb +67 -0
  129. data/lib/active_sanction/storage/meta.rb +188 -0
  130. data/lib/active_sanction/storage.rb +74 -0
  131. data/lib/active_sanction/subject.rb +271 -0
  132. data/lib/active_sanction/sync/report.rb +245 -0
  133. data/lib/active_sanction/sync/result.rb +296 -0
  134. data/lib/active_sanction/sync.rb +390 -0
  135. data/lib/active_sanction/validator_store/file_system.rb +103 -0
  136. data/lib/active_sanction/validator_store/memory.rb +41 -0
  137. data/lib/active_sanction/validator_store.rb +119 -0
  138. data/lib/active_sanction/validators.rb +195 -0
  139. data/lib/active_sanction/version.rb +23 -0
  140. data/lib/active_sanction.rb +331 -0
  141. data/lib/generators/active_sanction/install/install_generator.rb +60 -0
  142. data/lib/generators/active_sanction/install/templates/create_active_sanction_tables.rb.tt +142 -0
  143. metadata +236 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 63d579349c5ed0f731edbb3811cea143c2a1bdf7f591b319fb290ab6fb28c8c6
4
+ data.tar.gz: e6ad0e3c72fdca696dc2d9e2e1d528a4a43e638054362f1ebf0931def9b309c8
5
+ SHA512:
6
+ metadata.gz: '086eb3b75aa22d1cf3b976b269f28f49a0b5a734a22bba52f0a25119125042c40d41a35fc25b694069814d7f3d4e24426e51ef5da15f5d91f24c33519eff97f4'
7
+ data.tar.gz: d5cf18bc582c46e84b1255328e5a8411bb6e35c5444c8660c2687b7be5621411333f0d3a782fd352ad0963ab96e72ed93f1abd3fb7349bccc073f4f02e2e4452
data/CHANGELOG.md ADDED
@@ -0,0 +1,538 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
6
+ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ Two version numbers move independently in this project, and only one of them is this file's
9
+ subject. `ActiveSanction::VERSION` is the gem, and is what a release note is about.
10
+ `ActiveSanction::MATCHER_VERSION` is the matching pipeline, is stamped onto every
11
+ `MatchResult`, and is bumped whenever a change to the normalizer, the index, the similarity
12
+ algorithms or the scorer could move a score. **A change that moves `MATCHER_VERSION` is
13
+ called out here as such**, because it is the one kind of change that alters what a past
14
+ screening decision would come out as today.
15
+
16
+ ## [1.0.0] - 2026-09-13
17
+
18
+ The first release. Everything below is in it.
19
+
20
+ Why 1.0.0 and not 0.1.0: the public surface this ships is the one that was inventoried,
21
+ enumerated in [`docs/api_stability.md`](docs/api_stability.md) and held in place by
22
+ `spec/api_surface_spec.rb`, which fails the build in both directions. Shipping that under a
23
+ leading zero would say the opposite of what the inventory says. From here the deprecation
24
+ path in that document is in force — nothing enumerated there is removed without a warning
25
+ first, and a breaking change waits for 2.0.0.
26
+
27
+ ### Added
28
+
29
+ #### The canonical record
30
+
31
+ - `Entity`, one source-agnostic record model every adapter parses into: names, dates of birth,
32
+ addresses, identifiers, nationalities, programs, type, and the publisher's own text kept
33
+ verbatim in `remarks` ([#4](https://github.com/Babystep-Technologies/active_sanction/issues/4)).
34
+ - `Name` with alias kind and quality ([#5](https://github.com/Babystep-Technologies/active_sanction/issues/5)),
35
+ `PartialDate` for the year-only, approximate and ranged dates these lists actually publish
36
+ ([#6](https://github.com/Babystep-Technologies/active_sanction/issues/6)), and `Address` and
37
+ `Identifier` ([#7](https://github.com/Babystep-Technologies/active_sanction/issues/7)).
38
+ - `Snapshot`: one source's entities plus a checksum over their content, re-derived on
39
+ construction, so a truncated or edited list raises rather than screening quietly short
40
+ ([#8](https://github.com/Babystep-Technologies/active_sanction/issues/8)).
41
+
42
+ #### Fetching
43
+
44
+ - `HttpClient` with a mandatory User-Agent, bounded redirects and retries with backoff
45
+ ([#9](https://github.com/Babystep-Technologies/active_sanction/issues/9)).
46
+ - Conditional GET on ETag and Last-Modified, so an unchanged list costs one request and no
47
+ parse ([#10](https://github.com/Babystep-Technologies/active_sanction/issues/10)).
48
+ - `PayloadCache`, a bounded, integrity-verified cache of the raw bytes each publisher served
49
+ ([#11](https://github.com/Babystep-Technologies/active_sanction/issues/11)).
50
+
51
+ #### Sources
52
+
53
+ - `Sources::Base`, the adapter contract, and an open registry — a source registered from
54
+ outside this gem is a first-class source
55
+ ([#12](https://github.com/Babystep-Technologies/active_sanction/issues/12)), with a
56
+ declarative field-mapping DSL ([#13](https://github.com/Babystep-Technologies/active_sanction/issues/13)).
57
+ - `Parsers::DelimitedTable`, a reusable CSV toolkit
58
+ ([#14](https://github.com/Babystep-Technologies/active_sanction/issues/14)), and
59
+ `Parsers::XmlRecords`, a streaming XML toolkit with pluggable REXML and Nokogiri backends
60
+ ([#15](https://github.com/Babystep-Technologies/active_sanction/issues/15)).
61
+ - The shared `"a sanction source"` conformance group, and a spec that holds it to being able
62
+ to fail ([#16](https://github.com/Babystep-Technologies/active_sanction/issues/16)).
63
+ - **OFAC SDN** — the SDN, ALT and ADD CSVs, joined
64
+ ([#18](https://github.com/Babystep-Technologies/active_sanction/issues/18)).
65
+ - **OFAC Consolidated (non-SDN)** — the same shape, plus derivation of which of the six
66
+ sub-lists a record is on from its program codes, exact for 478 of 481 published records
67
+ ([#20](https://github.com/Babystep-Technologies/active_sanction/issues/20)).
68
+ - The OFAC remarks parser, which reads dates of birth, places of birth, nationalities and
69
+ passport numbers out of the one free-text field they are published in — currently
70
+ recognizing 97.3% of 88,827 segments, and reporting its own coverage per sync
71
+ ([#19](https://github.com/Babystep-Technologies/active_sanction/issues/19)).
72
+ - **UN Security Council consolidated list**
73
+ ([#21](https://github.com/Babystep-Technologies/active_sanction/issues/21)).
74
+ - **Canada SEMA / JVCFOA consolidated list**, with deterministic synthetic ids for a list that
75
+ publishes none ([#22](https://github.com/Babystep-Technologies/active_sanction/issues/22)).
76
+ - **EU Consolidated Financial Sanctions List (FSF)** — 6,234 records in one 25.7 MB document,
77
+ the first list large enough to exercise the streaming XML interface, read with no change to
78
+ core ([#39](https://github.com/Babystep-Technologies/active_sanction/issues/39)). Three
79
+ judgment calls in it are worth knowing about before relying on the list: the EU marks no
80
+ name as the official one, so a stated rule picks one; alias quality and alias kind are prose
81
+ in a per-name `<remark>` rather than a column, and are read from it; and four birth dates
82
+ are published in the Islamic calendar, three of which therefore produce no date of birth at
83
+ all rather than a Gregorian year in the fourteenth century that would *conflict* with the
84
+ real one. The endpoint does not honour conditional GET, so this is the one list that
85
+ downloads in full on every sync.
86
+ - **UK Sanctions List** — 6,334 designations in one 21.8 MB XML document, read with no change
87
+ to core ([#40](https://github.com/Babystep-Technologies/active_sanction/issues/40)). The
88
+ issue was scoped against OFSI's Consolidated List of Asset Freeze Targets, which the UK
89
+ retired on 28 January 2026 when it moved every designation onto one list; its blob still
90
+ answers 200 with a frozen 16.6 MB file, so an adapter reading it would look healthy on every
91
+ sync and screen against a list that stopped moving in January. **If you have your own
92
+ integration against `ConList.csv`, that is the thing to check today.** Two things about the
93
+ data are worth knowing before relying on it: a date component the FCDO does not know is
94
+ spelled out rather than omitted — `dd/mm/1962` is a year, `00/00/1975` is another spelling
95
+ of the same, and 824 of 3,788 birth dates carry one, every one of which reads as nil through
96
+ an ordinary date parser — and the publisher's own non-Latin script labels disagree with its
97
+ own strings on three records, which is why `Name#script` is left unstated here. The endpoint
98
+ honours conditional GET on both ETag and Last-Modified, so an unchanged list downloads
99
+ nothing.
100
+ - Four UK spellings added to `countries.txt` — `Congo (Democratic Republic)`, `St Kitts and
101
+ Nevis`, `St Lucia`, `St Vincent` — and `Palestinian` and `Occupied Palestinian Territories`,
102
+ which between them resolve 53 of the 63 UK nationality values that previously did not. Purely
103
+ additive: no existing spelling resolves differently, and the accuracy report is unchanged.
104
+ - **Australia's Consolidated List (DFAT)** — 3,906 records, read with no change to core
105
+ ([#41](https://github.com/Babystep-Technologies/active_sanction/issues/41)). The endpoint the
106
+ issue named was unverified and is now gone: `regulation8_consolidated.xlsx` redirects to a
107
+ `.xls` last modified in **March 2022**, which is served 200 and would look healthy on every
108
+ sync. This adapter reads the file DFAT's own page links today. Two things are worth knowing
109
+ before relying on it. **The Control Date is not a listing date** — DFAT defines it as when
110
+ the entry was last edited, it is on all 11,163 rows, and mapping it to `listed_on` would
111
+ report the Taliban listings of January 2001 as having been made this year; the real listing
112
+ date is prose, and is read on 1,438 of the 3,906 records. And **DFAT publishes no document
113
+ number of any kind** — no passport, no national identity number, no company registration —
114
+ so the only identifier on the list is an IMO number on 344 vessel rows, which makes a clean
115
+ Australian result weaker evidence than a clean OFAC one for the same reason a Canadian one
116
+ is. The endpoint also rejects this gem's User-Agent outright: its edge drops a request whose
117
+ leading product token it does not recognise, so this source sends the configured agent
118
+ inside `Mozilla/5.0 (compatible; …)` — the same identification, in a shape the edge parses.
119
+ It is the only place any source departs from `Sources::Base`.
120
+ - `Parsers::Spreadsheet`, a reusable `.xlsx` toolkit, **and no new dependency**. Australia
121
+ publishes its list as a spreadsheet and as nothing else, so reading one is the price of
122
+ screening against Australian sanctions at all — but an `.xlsx` is a ZIP of XML parts, `zlib`
123
+ is stdlib and this gem already reads XML, so what was missing was a ZIP header unpacker. It
124
+ resolves the shared string table and reads `xl/styles.xml`, which is not optional: `18798`
125
+ is a date if the cell is formatted as one and a year if it is not, and the Australian list
126
+ has 4,183 of the first and 2,709 of the second in the same column. Date cells arrive as ISO
127
+ 8601 at the precision the cell displays, which `PartialDate::Parser` reads directly.
128
+ - Two spellings added to `countries.txt` — `Democratic People's Republic of Korea (North
129
+ Korea)` and `Slovak Republic` — which resolve the only 2 of Australia's 77 nationality
130
+ values that previously did not. Purely additive: no existing spelling resolves differently.
131
+ - `docs/adding_a_source.md`, the end-to-end walkthrough for an eighth
132
+ ([#17](https://github.com/Babystep-Technologies/active_sanction/issues/17)).
133
+
134
+ #### Storage
135
+
136
+ - `Storage::Base`, five methods, and nothing on the query path naming a concrete store; plus
137
+ `Storage::Memory` ([#23](https://github.com/Babystep-Technologies/active_sanction/issues/23)).
138
+ - `Storage::FileSystem`, the gzipped-JSON default, committing with one atomic rename so an
139
+ interrupted sync leaves the previous list intact
140
+ ([#24](https://github.com/Babystep-Technologies/active_sanction/issues/24)).
141
+ - `Storage::ActiveRecord`, optional, with an indexed prefilter and a Rails install generator.
142
+ ActiveRecord is not a dependency of this gem and the adapter loads only where a host has
143
+ already loaded it ([#25](https://github.com/Babystep-Technologies/active_sanction/issues/25)).
144
+ - The shared `"a storage adapter"` conformance group, and a spec that holds it to being able
145
+ to fail.
146
+ - **The bundle format**: one list in one file that a different machine loads and trusts without
147
+ reaching the publisher, specified byte for byte in
148
+ [`docs/bundle_format.md`](docs/bundle_format.md) so that it can be produced and read outside
149
+ Ruby ([#57](https://github.com/Babystep-Technologies/active_sanction/issues/57)).
150
+ `ActiveSanction.export` writes one and `.import` loads it, over
151
+ `Snapshot::Bundle.write`/`.read`. Deterministic — the same snapshot always produces the same
152
+ bytes, since records are ordered by content, header keys have a fixed order, the compression
153
+ level is named by the specification and nothing in the file says when it was written — so two
154
+ mirrors of one list are comparable.
155
+ - Detached signatures over a bundle, `openssl` and nothing else. The signature covers the
156
+ header, which carries a digest of every record, so an unknown signer is refused *before* a
157
+ byte of what they sent is decompressed. Verification is opt-in and unsigned bundles stay
158
+ fully usable; a tampered file, a file signed by the wrong key, an unsigned file somebody
159
+ asked to verify, and a file from a newer gem each raise a different error, because each has a
160
+ different fix.
161
+ - `Snapshot#trusted?` and `MatchResult#verified?`, so a screening decision records whether the
162
+ list that answered it was attested. Deliberately in-memory: a signature covers a bundle's
163
+ bytes, not the copy a store rewrites into its own layout, so `trusted?` does not survive a
164
+ write to disk. `MatchResult` gains `verified` as the sixth field of its reproducibility
165
+ stamp — an addition to the serialized shape, which a record written without it reads back
166
+ as `false`.
167
+
168
+ #### Matching
169
+
170
+ - `Normalizer`: Unicode NFKD, mark stripping, casefolding, punctuation, whitespace, and a
171
+ table for the Latin letters decomposition cannot reach. One code path for the index and the
172
+ query, memoized and thread-safe
173
+ ([#26](https://github.com/Babystep-Technologies/active_sanction/issues/26)).
174
+ - Token dictionaries — legal forms, honorifics, organization stopwords, and a preserve list
175
+ that always wins — applied per entity type, as editable data files
176
+ ([#27](https://github.com/Babystep-Technologies/active_sanction/issues/27)).
177
+ - Jaro-Winkler and Levenshtein
178
+ ([#28](https://github.com/Babystep-Technologies/active_sanction/issues/28)), token sort and
179
+ token set ratios ([#29](https://github.com/Babystep-Technologies/active_sanction/issues/29)),
180
+ and Double Metaphone phonetic keys
181
+ ([#30](https://github.com/Babystep-Technologies/active_sanction/issues/30)) — all pure Ruby.
182
+ - An inverted index for candidate generation, keyed on tokens and phonetic codes
183
+ ([#31](https://github.com/Babystep-Technologies/active_sanction/issues/31)).
184
+ - `Scorer`: a 0-100 score that **is** the sum of its reasons, with secondary-identifier
185
+ adjustments on document numbers, dates of birth and nationality, an absent-is-not-conflict
186
+ rule, and early exits that are bounds rather than approximations — a thresholded call
187
+ returns exactly the scores an unthresholded one does
188
+ ([#32](https://github.com/Babystep-Technologies/active_sanction/issues/32)).
189
+ - `Country`, resolving both sides of a nationality comparison against a shipped ISO 3166-1
190
+ table, so `RU` meets `Russian Federation` and an unrecognized value is absent rather than a
191
+ contradiction.
192
+
193
+ #### The public API
194
+
195
+ - `Matcher`, `Query` and `MatchResult`, plus `ActiveSanction.screen` and `.screen_all`. A
196
+ matcher is immutable once built and screens from many threads without a lock; nothing on the
197
+ query path reads configuration
198
+ ([#33](https://github.com/Babystep-Technologies/active_sanction/issues/33)).
199
+ - Every `MatchResult` carries the snapshot checksum, matcher version, weights and query, and
200
+ round-trips losslessly through `to_h` / `from_h`, so a screening decision can be re-derived
201
+ by somebody who has neither this process nor this version of the gem.
202
+ - `ActiveSanction.sync!`: per-source failure isolation, a failed source keeping its previous
203
+ snapshot with a visible age, polite by-publisher concurrency, and a serializable report with
204
+ an exit code ([#34](https://github.com/Babystep-Technologies/active_sanction/issues/34)).
205
+ - `ActiveSanction.diff`: additions, delistings and amendments between two snapshots, joined by
206
+ entity id, so a book of business is re-screened against what moved
207
+ ([#35](https://github.com/Babystep-Technologies/active_sanction/issues/35)).
208
+ - `ActiveSanction.rescreen`: who a list change affects, which is the step that turns a diff
209
+ into an alert ([#60](https://github.com/Babystep-Technologies/active_sanction/issues/60)).
210
+ Screening a customer once is a checkbox; the obligation is ongoing, and the naive way to meet
211
+ it — every subject against every record, every night — costs the whole book times the whole
212
+ corpus. This costs the book times the handful of records that moved: 10,000 subjects against
213
+ a typical daily OFAC diff in 1.2 s, against 54 s to screen the same book against the whole
214
+ list.
215
+ - `Subject`, a book entry: the host's own stable id plus every evidence field `screen`
216
+ accepts, in every spelling it accepts them in. An alert names a customer rather than a
217
+ name, because a book screened by position cannot survive being filtered, streamed in
218
+ batches, or containing the same name twice.
219
+ - `Rescreen::Alert` classifies what happened to *the subject's match* — `newly_listed`,
220
+ `delisted`, `details_changed` — and carries a full `MatchResult` for each side of the
221
+ change, each stamped with the checksum of the list version it was scored against. Both
222
+ checksums are on the alert itself, so keeping one is keeping enough to derive the run
223
+ again, and the prior score is what lets it say a subject moved from 71 to 94 rather than
224
+ only that it now matches. It round-trips through `to_h` / `from_h` like a `MatchResult`.
225
+ - **A delisting raises an alert too.** It is a change of status a compliance team has to
226
+ record, and it is the half a re-screen against new records only would miss.
227
+ - **An amendment that does not move the score still raises one.** A program added or an
228
+ address corrected changes what a hit means without changing what it scores, and filtering
229
+ those would be deciding which sanctions hits a host is willing to miss.
230
+ - **A large book streams past a small diff.** Subjects are read one at a time and only alerts
231
+ are kept, the block is called with each alert as it is raised, and one `Rescreen` is
232
+ reusable across batches so its index is built once. Nothing here touches the matcher: a
233
+ rescreen indexes the diff and nothing else, so applying one never costs an index build over
234
+ the whole corpus.
235
+ - **An empty diff does no work at all** — not one subject is folded — which is what makes
236
+ rescreening after every sync affordable. A first sync is a baseline rather than a list of
237
+ additions, so it raises nothing either.
238
+ - `ActiveSanction.doctor`: whether a source's format has drifted, aggregated out of the health
239
+ signals a normal fetch and parse already produce
240
+ ([#68](https://github.com/Babystep-Technologies/active_sanction/issues/68)). It catches the
241
+ format change a sync cannot see — the one where the file still parses cleanly and means
242
+ something different, which today nothing would notice for months.
243
+ - **Field fill rates per source**, the check that catches a clean parse of a changed file:
244
+ 19,321 entities carrying zero passports looks exactly as healthy as 19,321 carrying 23,429
245
+ if the only thing counted is records. Measured over the records that could carry the field,
246
+ so a date of birth is a share of individuals.
247
+ - **The baseline is the last stored snapshot**, not a threshold committed per adapter — one
248
+ of those goes stale on its own, and the day somebody widens it to make a build pass is the
249
+ day it stops being read. Floors declared with `floor :remarks_coverage, 0.90` remain as a
250
+ coarse backstop for a run with nothing to compare against.
251
+ - **Column shape assertions for positional files.** OFAC ships headerless CSVs, so the
252
+ declared width catches a column *inserted* upstream and nothing catches one *reordered* —
253
+ which parses cleanly and builds entities out of shifted fields. `Parsers::ColumnShape`
254
+ asserts what the values are, not only how many there are.
255
+ - Free-text coverage, warning classes and orphaned child rows compared the same way, with the
256
+ severities meaning something specific: `error` is a reading no publisher could produce by
257
+ changing its *list*, `warn` is one a human should look at before the next sync.
258
+ - A serializable `Doctor::Report` with `exit_code` for cron and CI, and human-readable
259
+ `to_s` for the CLI verb to print. **The doctor writes nothing** — not the snapshot, not the
260
+ payload cache, not the conditional-GET validators — so diagnosing a source can never be the
261
+ reason a later sync decides it is unchanged, and nothing here repairs anything.
262
+ - `ActiveSanction.configure`, with a working default for every setting and a
263
+ `ConfigurationError` raised where a bad value is set rather than three hours into a sync. A
264
+ setting that does not exist is refused too, and the message names the ones that do.
265
+ - `Client`, the object a server holds, and the end of process-global configuration
266
+ ([#55](https://github.com/Babystep-Technologies/active_sanction/issues/55)). Everything at
267
+ the module level — `ActiveSanction.screen`, `.sync!`, `.diff`, `.doctor` — is now sugar over
268
+ a default client that `ActiveSanction.configure` populates, so the quickstart is unchanged
269
+ and a script never has to know this exists. What it buys is what a global could not express:
270
+ several configurations alive at once.
271
+ - `ActiveSanction::Client.new(storage:, sources:, user_agent:, ...)` takes every setting
272
+ `configure` takes, holds it frozen, and shares nothing with another client — its own store,
273
+ its own lists, its own index, its own publisher identity. Two clients screen against their
274
+ own data with no cross-talk, which is what a pinned list version for an audit re-run beside
275
+ the current one for live traffic, and a source set per tenant, both need.
276
+ - `Configuration` became that client's value object rather than global state. It is frozen
277
+ when a client is built, `#with` derives a mutable copy from a frozen one, and the default
278
+ store is settled at freeze rather than memoized on first read — so no two threads can race
279
+ to construct it.
280
+ - **The thread-safety contract is written down**: a built client and its loaded index are
281
+ safe to screen from concurrently, and `sync!` is safe alongside readers but is not
282
+ concurrent-safe against another sync of the same storage. See the README table.
283
+ - `ActiveSanction.reset!` (previously `reset_configuration!`) drops the default client
284
+ outright, which is what a suite runs between examples.
285
+ - A sync that fans out now carries the configuration it was started under into each worker
286
+ thread, so a client's User-Agent does not depend on `sync_concurrency:`; and the source
287
+ registry is built at load rather than on first write, so two adapters registering from two
288
+ threads cannot each create half of it.
289
+ - One documented error hierarchy under `ActiveSanction::Error`, which `rescue` catches
290
+ everything this library raises from a public method
291
+ ([#58](https://github.com/Babystep-Technologies/active_sanction/issues/58)):
292
+ `ConfigurationError`, `SourceError` (`FetchError`, `ParseError`, `IntegrityError`),
293
+ `StorageError`, `UnsupportedError`, `InvalidArgument` (`QueryError`) and `MissingKey`.
294
+ - Every error carries structured attributes rather than only a message: `source_id`,
295
+ `status`, `retryable?` and `to_h`. `retryable?` is first-class, so a host application
296
+ builds backoff from a predicate instead of from message strings — a 503 or a timeout is
297
+ retryable, a 403 or a parse failure is not, and a misconfiguration never is.
298
+ - `ParseError` says *where*: `line`, `record` or `offset`, appended to its own message, so a
299
+ 25 MB payload that turns out not to be XML is diagnosable.
300
+ - The list a failure belongs to is stamped on as the error leaves the adapter, since the
301
+ layer that raises usually cannot know it — the HTTP client sees a URL.
302
+ - No public method leaks an exception class from `net/http`, `openssl`, `csv`, `rexml`,
303
+ `nokogiri`, `zlib` or `json`, and nothing raises a bare `RuntimeError` or `ArgumentError`.
304
+ `InvalidArgument` is an `::ArgumentError` and `MissingKey` a `::KeyError`, so surrounding
305
+ code keeps the rescue it already has; `ActiveSanction::Error` is a module so that both can
306
+ be in the hierarchy anyway.
307
+
308
+ #### Measurement and tooling
309
+
310
+ - `rake benchmark:rescreen`, which measures applying a diff to a book of business against the
311
+ naive full rescreen it replaces, and sweeps how the cost moves with how much the list did
312
+ ([#60](https://github.com/Babystep-Technologies/active_sanction/issues/60)).
313
+ - `rake benchmark:accuracy` and `rake benchmark:latency`, an 87-query labeled set, and a
314
+ **committed** accuracy report — a diff in
315
+ [`benchmark/results/accuracy.md`](benchmark/results/accuracy.md) is a change in what this
316
+ library finds ([#37](https://github.com/Babystep-Technologies/active_sanction/issues/37)).
317
+ The default threshold of 75 is where F1 peaks on that set, measured rather than chosen.
318
+ - **The upstream canary**: a scheduled workflow that fetches all seven lists from their real
319
+ publishers on weekdays, parses them, and compares what it measures against the baselines
320
+ committed under [`.github/baselines`](.github/baselines)
321
+ ([#69](https://github.com/Babystep-Technologies/active_sanction/issues/69)). It is
322
+ `ActiveSanction.doctor` pointed at a file instead of a snapshot, and it is for the
323
+ maintainer rather than the operator — a downstream doctor warning that OFAC's remarks
324
+ vocabulary moved can only ever result in an issue filed here, because the label table lives
325
+ here.
326
+ - **The output is a GitHub issue, one per source**, opened with the findings, rewritten by
327
+ every run that still finds something, and closed by the first run that comes back clean.
328
+ Deliberately not a red badge: this never runs as part of CI, because a red build should
329
+ mean our code broke rather than that a source went down.
330
+ - **A fetch failure is reported separately from a parse difference**, and nothing is opened
331
+ until two consecutive runs agree about it. Government endpoints 403 a non-browser user
332
+ agent and block cloud IP ranges, and a canary that cried wolf on one bad afternoon would
333
+ be muted inside a week. Each run keeps its report as a workflow artifact and the next run
334
+ confirms against it.
335
+ - **The baseline is a committed file, with tolerances per key** — 5% on a record count,
336
+ which moves every business day, and 2% on free-text coverage, which does not move on its
337
+ own at all. A diff in `.github/baselines` is a change in what a government publishes, and
338
+ a clean run opens a rolling pull request keeping those numbers current.
339
+ - It found something on its first run: OFAC's Consolidated list inherited the SDN file's
340
+ 90% remarks-coverage floor and reads at 80.3%, because the CMIC rows publish a vocabulary
341
+ — `Purchase/Sales For Divestment`, `Equity Ticker`, `HKAA Section 5` — that has no
342
+ equivalent on the SDN file and nothing for this parser to do with it. The floor is now
343
+ declared on the Consolidated adapter at 0.75, so a first `doctor` run against a fresh
344
+ deployment no longer warns about a list that is doing exactly what it always does.
345
+ - Benchmarks for the similarity algorithms, the index and the scorer
346
+ ([#28](https://github.com/Babystep-Technologies/active_sanction/issues/28),
347
+ [#31](https://github.com/Babystep-Technologies/active_sanction/issues/31),
348
+ [#32](https://github.com/Babystep-Technologies/active_sanction/issues/32)).
349
+ - Sorbet at `typed: strict` across `lib/`, with per-query signatures declared
350
+ `.checked(:tests)` and a supported way for a host to turn every runtime check off
351
+ ([#73](https://github.com/Babystep-Technologies/active_sanction/issues/73)).
352
+ - CI across every Ruby the gem supports — 3.1, 3.2, 3.3, 3.4 and 4.0, plus a non-blocking
353
+ `ruby-head` — with the matrix, `.ruby-version`, `required_ruby_version` and RuboCop's
354
+ `TargetRubyVersion` held to each other by a spec, so the version a change is developed on
355
+ cannot again be the one version no build runs
356
+ ([#2](https://github.com/Babystep-Technologies/active_sanction/issues/2),
357
+ [#80](https://github.com/Babystep-Technologies/active_sanction/issues/80)).
358
+ - A hermetic suite — an un-stubbed HTTP call fails rather than quietly reaching a government
359
+ server ([#3](https://github.com/Babystep-Technologies/active_sanction/issues/3)).
360
+ - The same suite isolates global configuration: `ActiveSanction.configure` replaces the
361
+ process-wide default client and nothing put it back, so an example that configured anything
362
+ decided what every example RSpec ran after it saw, and the build passed or failed on its
363
+ seed. The configuration is reset after every example
364
+ ([#123](https://github.com/Babystep-Technologies/active_sanction/issues/123)).
365
+
366
+ #### The documentation site
367
+
368
+ - **Scaffold, build and deploy for a GitHub Pages site**, built with Astro and Starlight and
369
+ structured by the four Diátaxis quadrants
370
+ ([#103](https://github.com/Babystep-Technologies/active_sanction/issues/103)). Dark mode,
371
+ a responsive sidebar and full-text search come from the theme rather than from anything
372
+ written here. Sources live in `site/`, not in `docs/` — building from `docs/` would ship a
373
+ config file, a theme and a set of layouts into every application that installs this gem,
374
+ and `docs/` is in the package on purpose. `spec/licensing_spec.rb` now holds both halves:
375
+ `site/` never ships, and `docs/` always does.
376
+ - **Navigation is named for what a reader wants, not for the framework.** Get started,
377
+ Guides, Reference, Explanation. Diátaxis is the discipline for whoever writes the pages;
378
+ nobody arrives at a documentation site wanting a quadrant.
379
+ - **Generated API documentation is published under `/api/`**, built by `rake doc` during
380
+ deploy and linked from the navigation, so the handwritten reference can point into a
381
+ signature rather than restating one.
382
+ - **A link checker that gates on internal links and anchors, and not on external ones.** A
383
+ link into a page that no longer has that heading still lands somewhere real, at the top,
384
+ silently — which is the breakage worth catching. External links are counted and not
385
+ fetched: half of them point at government publishers that 403 a non-browser user agent on
386
+ purpose, and a build that went red when Treasury rate-limited a runner would be muted
387
+ inside a week. Same reasoning that keeps the canary out of CI.
388
+ - **Every fenced Ruby sample on the site declares itself `runnable` or `illustrative`**, and
389
+ `spec/site_samples_spec.rb` runs the first kind, parses both, and fails on a block that
390
+ declares neither. An illustrative block has to say why it cannot run. The dangerous sample
391
+ is not the one somebody marked wrong — it is the one nobody thought about, which looks
392
+ exactly like a tested one to a reader.
393
+ - **Generated pages are skipped as a source of links, while staying a valid target.** YARD
394
+ renders the README as its index page, where every relative link in it resolves against the
395
+ repository rather than against the site; crawling that output reported 126 broken links
396
+ that were all correct where they were written. The navigation's own link to `/api/` is
397
+ still checked, which is what catches a deploy that forgot to copy it.
398
+ - Deploys from a workflow rather than from a branch, so the build runs the link check before
399
+ anything is published and a pull request gets the same check without publishing. `rake
400
+ site:check` reproduces that whole sequence locally, in the same order. The site pins its
401
+ own Node, deliberately separate from the gem's 3.1-to-4.0 Ruby matrix: an Astro release
402
+ must never be the reason the library's build goes red.
403
+
404
+ #### Instructions for a coding agent
405
+
406
+ - **Two skills under `.claude/skills/`**, so an agent asked to add a sanctions list, or to fix
407
+ one whose publisher changed its format, arrives with the procedure already loaded instead of
408
+ inferring it ([#113](https://github.com/Babystep-Technologies/active_sanction/issues/113)).
409
+ `adding-a-source` is the walkthrough compressed into a working order; `repairing-a-source` is
410
+ the canary-to-fix loop. Two rather than one, because they are two jobs with different failure
411
+ modes: adding a list goes wrong by guessing at a rule nobody would guess, and repairing one
412
+ goes wrong by fixing the parse while quietly changing what a field means.
413
+ - **The reason this is in the gem rather than in somebody's dotfiles** is that
414
+ `docs/adding_a_source.md` is 1,083 lines written for a human reading it start to finish, and
415
+ an agent does not read it that way. It greps, finds section 5, and misses the three rules in
416
+ section 6 that make an id stable — and the failure is silent, because the adapter parses, the
417
+ spec passes, and the ids change on every sync. A skill costs a contributor who does not use
418
+ one nothing, sits next to the code it describes, and goes stale visibly.
419
+ - **One copy of the rules, in `.claude/rules/adapter-rules.md`**, which both skills read: every
420
+ date is a `PartialDate`, an id never depends on anything outside the record's own bytes, the
421
+ publisher's free text is kept verbatim, a fixture is trimmed from a real published file and
422
+ never written by hand, and no skill fetches a government endpoint on its own — publishers 403
423
+ non-browser user agents, so the command is named and a person runs it. Each rule links to the
424
+ section of `docs/adding_a_source.md` that argues for it rather than restating the argument.
425
+ - **`spec/agent_instructions_spec.rb` is what makes "goes stale visibly" true.** Every relative
426
+ link in an agent-facing document has to resolve and every anchor has to name a heading that
427
+ still exists, because a pointer into a retitled section still lands somewhere real, at the top,
428
+ silently — the same breakage the site's link checker exists for. It also holds the skill
429
+ frontmatter to its shape, including the unquoted ` #` that YAML reads as a comment and
430
+ truncates a description at without an error anywhere.
431
+ - **`AGENTS.md` for tools that do not read Claude skills**, as a thin pointer to
432
+ `CONTRIBUTING.md` rather than a third copy of the same rules.
433
+ - **None of it ships.** `.claude/` was already excluded by the gemspec's leading-dot rule and
434
+ `AGENTS.md` is now named alongside `Gemfile` and `Rakefile`: both are instructions for working
435
+ on this repository and say nothing to an application that installed the gem.
436
+
437
+ #### API stability, and what may change
438
+
439
+ - **The public surface is enumerated rather than inferred**, in
440
+ [`docs/api_stability.md`](docs/api_stability.md)
441
+ ([#62](https://github.com/Babystep-Technologies/active_sanction/issues/62)). Roughly 500
442
+ constants are reachable from `ActiveSanction`; **137 are promised**. The rest are marked
443
+ `@api private`, hidden from the rendered documentation, and may be renamed or removed in
444
+ a patch release. Every constant a user can reach is one somebody will reach, and without
445
+ a stated boundary an internal becomes load-bearing by accident.
446
+ - [`spec/api_surface_spec.rb`](spec/api_surface_spec.rb) fails the build when the code and
447
+ that document disagree **in either direction** — a new public constant nobody wrote down,
448
+ or a name written down that no longer exists. Widening the surface is now a diff somebody
449
+ reviewed rather than a side effect of adding a class.
450
+ - **SemVer, with the pre-1.0 rule said out loud.** Before 1.0 a minor version may break the
451
+ public API — `0.4.0` may remove what `0.3.0` promised, which is what the leading zero
452
+ means — and a patch release never does.
453
+ - **`ActiveSanction::Deprecation`**, and one full minor release of overlap. Something
454
+ deprecated in `0.4.0` works through all of `0.5.x` and may be removed in `0.6.0`, so an
455
+ application upgrading one minor at a time always meets the warning at least one release
456
+ before the breakage. `removal_for` computes that version rather than leaving it to be
457
+ remembered.
458
+ - Warnings go through `Kernel#warn` with `category: :deprecated`, so
459
+ `Warning[:deprecated] = false` silences them — the line a host already has in their
460
+ `spec_helper`, rather than a setting of ours they would have to discover.
461
+ - Each call site warns once however many times it is reached: a deprecated method called
462
+ while looping over 19,000 records writes one line, not 19,000. Working out *which* call
463
+ site took some care, because `sig` wraps every method here and Sorbet's validation
464
+ wrapper both hides the real caller and moves once the fast path is swapped in.
465
+ - **`Sources::Base`, `Storage::Base` and `ValidatorStore` carry the strongest guarantee.**
466
+ Breaking one forks every adapter written outside this repository at once, and those
467
+ authors are not reading these release notes. The two conformance groups are the
468
+ executable statement of what each requires.
469
+ - `rake doc` now renders the public surface only, at **100% documented**. The 140 constants
470
+ YARD reported as undocumented were internal — column names, regex fragments, the `MEMBERS`
471
+ lists a value object serializes through — and the answer to them was a boundary rather
472
+ than 140 comments restating their names
473
+ ([#38](https://github.com/Babystep-Technologies/active_sanction/issues/38)).
474
+ - Named as **deliberately not public**: the matching internals (`Index`, `Similarity`,
475
+ `Phonetics`, and the scorer's `Adjustments` and `NameScore`), `HttpClient`, `Fetcher` and
476
+ `PayloadCache` as classes though their errors are public, every `MEMBERS` list, the
477
+ per-adapter `Record` classes, and the parser toolkits' readers and backends.
478
+
479
+ #### Governance
480
+
481
+ - Contributions are accepted under the
482
+ [Developer Certificate of Origin](https://developercertificate.org) — a `Signed-off-by`
483
+ trailer, which `git commit -s` writes and which a CI job checks on every pull request,
484
+ printing the rebase that fixes a branch already pushed
485
+ ([#61](https://github.com/Babystep-Technologies/active_sanction/issues/61)). **There is no
486
+ contributor licence agreement.** The commercial advantage here is operational rather than
487
+ code secrecy, so there is no right to relicense worth reserving, and a contributor pays one
488
+ flag instead of a signature.
489
+ - **The licence stays MIT, and that was a decision rather than a default.** A move to
490
+ Apache-2.0 was considered and declined: this library implements no patentable technique —
491
+ Jaro-Winkler, Levenshtein, token-set ratios and Double Metaphone are all long published and
492
+ none encumbered — so an express patent grant would defend against a thicket that does not
493
+ exist, and MIT is the lowest-friction signal in an ecosystem that is overwhelmingly MIT.
494
+ `spec/licensing_spec.rb` holds `LICENSE.txt`, the gemspec and the README to saying the same
495
+ thing, because a licence file and a `spec.license` that disagree are read by different
496
+ audiences and neither one notices.
497
+ - `CONTRIBUTING.md` and `SECURITY.md`, both shipped **inside the gem** rather than only on
498
+ GitHub — a dependency is often audited from a vendored bundle or an air-gapped host, and the
499
+ address to report a vulnerability to is exactly what that reader is looking for.
500
+ - **`SECURITY.md` treats a false negative as a security bug.** A screening library that fails
501
+ to report a listed name is not merely inaccurate; somebody may be relying on an empty result
502
+ to clear a payment. So a systematic screening bypass, a bundle signature that verifies when
503
+ it should not, and any path by which a screened name leaves the host process are all in
504
+ scope for private disclosure — while a single wrong score stays a public issue and a row in
505
+ the labeled set.
506
+ - Issue forms for a bug, a name this version gets wrong, and a list the gem does not read yet;
507
+ a pull request template; and `CODEOWNERS`.
508
+ - **A trademark note in the README and `CONTRIBUTING.md`.** MIT grants no rights in the name
509
+ either way — this says so out loud, so a fork does not have to guess. The bundle format
510
+ stays open and unencumbered: anyone can produce one.
511
+
512
+ ### Known limitations at this release
513
+
514
+ Documented in full in the README under
515
+ [Known data limitations, per source](README.md#known-data-limitations-per-source), and
516
+ summarized here because they are what a reader of a first release most needs:
517
+
518
+ - Seven lists, across six jurisdictions: `ofac_sdn`, `ofac_consolidated`, `un_consolidated`,
519
+ `canada_sema`, `eu_fsf`, `uk_sanctions_list` and `australia_dfat`. Nowhere else is read, and
520
+ a name absent from all seven has been screened against those seven and nothing more.
521
+ - Non-Latin script is not transliterated. A Cyrillic name matches a Cyrillic query and nothing
522
+ else.
523
+ - OFAC's secondary identifiers come from heuristic parsing of free text, at 97.3% segment
524
+ coverage.
525
+ - Canada publishes no nationality, address, place of birth or document number at all, and no
526
+ identifier of its own — so its ids are synthetic, and a clean Canadian result is weaker
527
+ evidence than a clean OFAC one.
528
+ - The EU publishes no primary name and no alias-quality column, so which of a record's names
529
+ is called primary is this library's rule rather than the Commission's, and most EU aliases
530
+ arrive ungraded. Its endpoint ignores conditional GET, so every sync of it transfers 25.7 MB.
531
+ - The labeled accuracy set covers four of the seven lists — OFAC SDN, OFAC Consolidated, UN
532
+ and Canada — so the committed recall and precision figures describe those. The EU, UK and
533
+ Australian adapters each have their own spec, and the conformance group holds all seven to
534
+ the same floor, but the accuracy report does not yet speak for the last three.
535
+ - Recall at the default threshold is 0.939 overall on the labeled set, and every record this
536
+ version misses is named in the committed accuracy report.
537
+
538
+ [1.0.0]: https://github.com/Babystep-Technologies/active_sanction/releases/tag/v1.0.0
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at shen.marshall@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [https://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: https://contributor-covenant.org
74
+ [version]: https://contributor-covenant.org/version/1/4/