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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +538 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +312 -0
- data/LICENSE.txt +21 -0
- data/README.md +616 -0
- data/SECURITY.md +97 -0
- data/active_sanction.gemspec +98 -0
- data/docs/adding_a_source.md +1083 -0
- data/docs/api_stability.md +357 -0
- data/docs/bundle_format.md +407 -0
- data/lib/active_sanction/address.rb +170 -0
- data/lib/active_sanction/client.rb +341 -0
- data/lib/active_sanction/configuration.rb +721 -0
- data/lib/active_sanction/countries.txt +292 -0
- data/lib/active_sanction/country.rb +164 -0
- data/lib/active_sanction/deprecation.rb +168 -0
- data/lib/active_sanction/diff/change.rb +208 -0
- data/lib/active_sanction/diff.rb +348 -0
- data/lib/active_sanction/doctor/checkup.rb +356 -0
- data/lib/active_sanction/doctor/diagnosis.rb +245 -0
- data/lib/active_sanction/doctor/finding.rb +183 -0
- data/lib/active_sanction/doctor/profile.rb +384 -0
- data/lib/active_sanction/doctor/report.rb +227 -0
- data/lib/active_sanction/doctor.rb +347 -0
- data/lib/active_sanction/entity.rb +293 -0
- data/lib/active_sanction/error.rb +332 -0
- data/lib/active_sanction/fetcher/result.rb +116 -0
- data/lib/active_sanction/fetcher.rb +242 -0
- data/lib/active_sanction/http_client/errors.rb +94 -0
- data/lib/active_sanction/http_client/response.rb +129 -0
- data/lib/active_sanction/http_client.rb +324 -0
- data/lib/active_sanction/identifier.rb +212 -0
- data/lib/active_sanction/index/builder.rb +89 -0
- data/lib/active_sanction/index/candidate.rb +63 -0
- data/lib/active_sanction/index/entry.rb +66 -0
- data/lib/active_sanction/index/features.rb +112 -0
- data/lib/active_sanction/index.rb +381 -0
- data/lib/active_sanction/match_result.rb +345 -0
- data/lib/active_sanction/matcher.rb +388 -0
- data/lib/active_sanction/name.rb +183 -0
- data/lib/active_sanction/normalizer/cache.rb +82 -0
- data/lib/active_sanction/normalizer/dictionaries/honorifics.txt +30 -0
- data/lib/active_sanction/normalizer/dictionaries/legal_forms.txt +72 -0
- data/lib/active_sanction/normalizer/dictionaries/organization_stopwords.txt +20 -0
- data/lib/active_sanction/normalizer/dictionaries/particles.txt +35 -0
- data/lib/active_sanction/normalizer/dictionary/stoplist.rb +121 -0
- data/lib/active_sanction/normalizer/dictionary.rb +243 -0
- data/lib/active_sanction/normalizer/form.rb +279 -0
- data/lib/active_sanction/normalizer.rb +137 -0
- data/lib/active_sanction/parsers/column_shape.rb +274 -0
- data/lib/active_sanction/parsers/delimited_table/reader.rb +194 -0
- data/lib/active_sanction/parsers/delimited_table/row.rb +81 -0
- data/lib/active_sanction/parsers/delimited_table.rb +151 -0
- data/lib/active_sanction/parsers/format.rb +107 -0
- data/lib/active_sanction/parsers/join.rb +149 -0
- data/lib/active_sanction/parsers/spreadsheet/archive.rb +258 -0
- data/lib/active_sanction/parsers/spreadsheet/reader.rb +213 -0
- data/lib/active_sanction/parsers/spreadsheet/row.rb +88 -0
- data/lib/active_sanction/parsers/spreadsheet/workbook.rb +314 -0
- data/lib/active_sanction/parsers/spreadsheet.rb +168 -0
- data/lib/active_sanction/parsers/xml_records/backends/nokogiri.rb +134 -0
- data/lib/active_sanction/parsers/xml_records/backends/rexml.rb +157 -0
- data/lib/active_sanction/parsers/xml_records/backends.rb +103 -0
- data/lib/active_sanction/parsers/xml_records/builder.rb +90 -0
- data/lib/active_sanction/parsers/xml_records/reader.rb +134 -0
- data/lib/active_sanction/parsers/xml_records/record.rb +174 -0
- data/lib/active_sanction/parsers/xml_records.rb +140 -0
- data/lib/active_sanction/parsers.rb +94 -0
- data/lib/active_sanction/partial_date/parser.rb +124 -0
- data/lib/active_sanction/partial_date.rb +303 -0
- data/lib/active_sanction/payload_cache/checksum.rb +66 -0
- data/lib/active_sanction/payload_cache/entry.rb +304 -0
- data/lib/active_sanction/payload_cache.rb +366 -0
- data/lib/active_sanction/phonetics/double_metaphone.rb +662 -0
- data/lib/active_sanction/phonetics.rb +40 -0
- data/lib/active_sanction/query.rb +290 -0
- data/lib/active_sanction/rescreen/alert.rb +314 -0
- data/lib/active_sanction/rescreen.rb +332 -0
- data/lib/active_sanction/scorer/adjustments.rb +241 -0
- data/lib/active_sanction/scorer/name_score.rb +279 -0
- data/lib/active_sanction/scorer/reason.rb +136 -0
- data/lib/active_sanction/scorer/result.rb +129 -0
- data/lib/active_sanction/scorer/subject.rb +235 -0
- data/lib/active_sanction/scorer/weights.rb +306 -0
- data/lib/active_sanction/scorer.rb +303 -0
- data/lib/active_sanction/similarity/jaro_winkler.rb +210 -0
- data/lib/active_sanction/similarity/levenshtein.rb +179 -0
- data/lib/active_sanction/similarity/token_set.rb +159 -0
- data/lib/active_sanction/similarity/token_sort.rb +120 -0
- data/lib/active_sanction/similarity.rb +183 -0
- data/lib/active_sanction/snapshot/bundle/header.rb +302 -0
- data/lib/active_sanction/snapshot/bundle/payload.rb +185 -0
- data/lib/active_sanction/snapshot/bundle/signature.rb +201 -0
- data/lib/active_sanction/snapshot/bundle.rb +381 -0
- data/lib/active_sanction/snapshot.rb +305 -0
- data/lib/active_sanction/sources/australia_dfat/published_date.rb +166 -0
- data/lib/active_sanction/sources/australia_dfat/record.rb +319 -0
- data/lib/active_sanction/sources/australia_dfat.rb +291 -0
- data/lib/active_sanction/sources/base.rb +295 -0
- data/lib/active_sanction/sources/canada_sema/record.rb +282 -0
- data/lib/active_sanction/sources/canada_sema/source_ref.rb +89 -0
- data/lib/active_sanction/sources/canada_sema.rb +153 -0
- data/lib/active_sanction/sources/definition.rb +360 -0
- data/lib/active_sanction/sources/eu_fsf/record.rb +467 -0
- data/lib/active_sanction/sources/eu_fsf.rb +203 -0
- data/lib/active_sanction/sources/ofac/record.rb +251 -0
- data/lib/active_sanction/sources/ofac/remarks_parser/coverage.rb +120 -0
- data/lib/active_sanction/sources/ofac/remarks_parser/vocabulary.rb +121 -0
- data/lib/active_sanction/sources/ofac/remarks_parser.rb +312 -0
- data/lib/active_sanction/sources/ofac.rb +248 -0
- data/lib/active_sanction/sources/ofac_consolidated/record.rb +37 -0
- data/lib/active_sanction/sources/ofac_consolidated.rb +203 -0
- data/lib/active_sanction/sources/ofac_sdn.rb +43 -0
- data/lib/active_sanction/sources/remarks.rb +84 -0
- data/lib/active_sanction/sources/uk_sanctions_list/published_date.rb +104 -0
- data/lib/active_sanction/sources/uk_sanctions_list/record.rb +412 -0
- data/lib/active_sanction/sources/uk_sanctions_list.rb +214 -0
- data/lib/active_sanction/sources/un_consolidated/record.rb +279 -0
- data/lib/active_sanction/sources/un_consolidated.rb +140 -0
- data/lib/active_sanction/sources.rb +196 -0
- data/lib/active_sanction/storage/active_record/reader.rb +124 -0
- data/lib/active_sanction/storage/active_record/row.rb +175 -0
- data/lib/active_sanction/storage/active_record/writer.rb +141 -0
- data/lib/active_sanction/storage/active_record.rb +277 -0
- data/lib/active_sanction/storage/base.rb +216 -0
- data/lib/active_sanction/storage/file_system.rb +373 -0
- data/lib/active_sanction/storage/memory.rb +67 -0
- data/lib/active_sanction/storage/meta.rb +188 -0
- data/lib/active_sanction/storage.rb +74 -0
- data/lib/active_sanction/subject.rb +271 -0
- data/lib/active_sanction/sync/report.rb +245 -0
- data/lib/active_sanction/sync/result.rb +296 -0
- data/lib/active_sanction/sync.rb +390 -0
- data/lib/active_sanction/validator_store/file_system.rb +103 -0
- data/lib/active_sanction/validator_store/memory.rb +41 -0
- data/lib/active_sanction/validator_store.rb +119 -0
- data/lib/active_sanction/validators.rb +195 -0
- data/lib/active_sanction/version.rb +23 -0
- data/lib/active_sanction.rb +331 -0
- data/lib/generators/active_sanction/install/install_generator.rb +60 -0
- data/lib/generators/active_sanction/install/templates/create_active_sanction_tables.rb.tt +142 -0
- 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
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -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/
|