API stability
What this gem promises not to break, and what it reserves the right to change in any release.
The public surface is enumerated below, not inferred. A constant being
reachable does not make it public; over 500 of them are reachable and 142 are
promised. Everything else is marked @api private in the source, is hidden
from the rendered documentation, and may be renamed, moved or deleted in a
patch release without a note anywhere. If you need something that is not on
this list, open an issue rather than reaching for it — the whole point of
writing the boundary down is that widening it is a conversation rather than an
accident.
spec/api_surface_spec.rb reads this file and
fails when the code and this list stop agreeing, in either direction. A new
public constant that nobody added here fails the suite; a name here that no
longer exists fails it too.
Versioning
This project follows Semantic Versioning.
The first release is 1.0.0, and there was no 0.x. A leading zero says a
minor version may remove what the last one promised, and that is not what this
library is doing: the surface below was inventoried before it shipped and
spec/api_surface_spec.rb fails the build in both directions. Publishing an
enumerated, test-enforced surface under a number that means this may move is
a contradiction, so the number matches the inventory instead.
So the deprecation path below is in force from 1.0.0. Nothing enumerated here is removed without a warning first, and a breaking change waits for 2.0.0. A patch release never breaks anything.
Two version numbers move independently, and only one of them is what this document is about:
| | |
|---|---|
| ActiveSanction::VERSION | The gem. This document governs it. |
| ActiveSanction::MATCHER_VERSION | The matching pipeline. Bumped whenever a change could move a score. |
MATCHER_VERSION is not a compatibility promise and never gates a rescue or a
version constraint. It is stamped onto every MatchResult so that an auditor
asking would this screening have come out the same? has an answer that a gem
version cannot give them — a release that only adds a source adapter moves one
of these numbers and not the other.
Deprecation
Nothing on the enumerated list is removed without a warning first.
- The release that deprecates something keeps it working and warns.
- It goes on working for one full minor release after that one.
- It may be removed in the minor after that.
So something deprecated in 1.4.0 works through all of 1.5.x and may be
removed in 1.6.0. An application that upgrades one minor at a time always
meets the warning at least one release before the breakage.
ActiveSanction::Deprecation.removal_for computes that date rather than
leaving it to be remembered, and the warning says it out loud:
active_sanction: ActiveSanction.old_thing is deprecated since 1.4.0 and will
be removed in 1.6.0. Use ActiveSanction.new_thing instead.
Called from app/jobs/screen_job.rb:31
Warnings go through Ruby's own switch. They are Kernel#warn with
category: :deprecated, so Warning[:deprecated] = false silences them —
the same line that silences every other deprecation in a Ruby process, rather
than a setting of ours that has to be discovered. Ruby's default is off outside
verbose mode, which is kept: the audience for a deprecation is a developer
running ruby -w, a suite, or a CI build.
Each call site warns once, however many times it is reached. A deprecated method called while looping over 19,000 records writes one line, not 19,000.
Every deprecation also gets a CHANGELOG.md entry under Deprecated in the
release that introduces it, and a second under Removed in the release that
carries it out.
The extension points carry the strongest guarantee
Three of these are load-bearing for code that is not in this repository:
ActiveSanction::Sources::Base
ActiveSanction::Storage::Base
ActiveSanction::ValidatorStore
Breaking one of them forks every downstream adapter at once — a bank's internal
watchlist, a store backed by somebody's own database — and those authors are
not reading this repository's release notes. So the required methods of each,
their arguments and what they must return do not change within a major version,
and the shared conformance groups
(lib/active_sanction/testing/sanction_source.rb,
lib/active_sanction/testing/storage_adapter.rb)
are the executable statement of what they require. An adapter that passes them
today passes them for the life of the major version — and they ship, so an
adapter outside this repository can run them: require "active_sanction/testing".
New optional hooks may be added — a method with a default implementation on the base class is not a break, because an adapter that does not define it goes on working.
Contracts that are not constants
Five promises here are about behaviour rather than about a name, and none of them is enforceable by the surface spec.
The error hierarchy. Within a major version an error does not move to a
different parent, and no attribute is removed from one. New subclasses may
appear under an existing parent — that is what keeps rescue FetchError
working when a new transport failure earns a name of its own — so a case over
error classes wants an else. retryable?, source_id, status and to_h
are part of the promise; the wording of a message is not, and was never
something to match on.
The bundle format is specified in bundle_format.md,
which is the contract — not the Ruby constants that implement it, which are
private. A bundle carries its own format version, and this gem reads every
version in its declared readable range. The format is open and unencumbered:
anyone may produce or consume one, in any language.
The stored snapshot format carries a schema version for the same reason. A store written against one schema version keeps being readable; the file layout underneath a shipped store is not public and may change.
The instrumentation events. ActiveSanction::Instrumentation::EVENTS
names the six, and each one's payload keys are promised the same way a method
signature is: within a major version a key is not removed, renamed, or made to
mean something else, and a dashboard written against one keeps working. Keys
may be added to an event — that is how a new measurement ships without a
major version — so a subscriber reads the keys it knows and ignores the rest,
and must not assume the set is closed.
Every event carries name, started_at and duration, plus the keys below.
error is present only when the stage raised, in which case the keys it had
not reached yet are absent rather than zero.
| Event | Payload keys |
|---|---|
:fetch |
source, key, url, forced, conditional, status, not_modified, bytes |
:parse |
source, bytes, records, warnings |
:store |
source, snapshot_id, entities, store, imported (on an import only) |
:"index.build" |
store, sources, snapshots, entities, names, keys, postings, bytes |
:screen |
candidates, scored, results, threshold, limit, sources, snapshots |
:sync |
sources, forced, concurrency, outcomes, updated, unchanged, failed, records |
Two things about them are contracts rather than incidental. A :fetch event
is one HTTP round trip — a file served out of the payload cache after a 304
costs no request and emits nothing, and a source re-fetching one because its
cache was empty emits a second event rather than amending the first. And
bytes on :"index.build" is an estimate, documented as one on
Index#profile; it is good to within a factor a dashboard cares about and is
not a heap measurement.
A MatchResult is reproducible. The snapshot checksum, matcher version,
weights and query it stamps are what let a screening decision be re-derived
years later. Fields may be added to that stamp; the meaning of an existing one
does not change under it.
What is deliberately not public
Named here because their absence from the list is a decision rather than an oversight:
- The instrumenter contract's other half. A subscriber is anything
answering
#call(event), and that is public.Instrumentation.instrumentand.emit, which the library's own stages call, are not: where an event is emitted from is an implementation detail of the stage, and a host that wanted to emit one of these names itself would be publishing a measurement of something this library did not do. - The matching internals —
Index,Similarity,Phonetics, and the scorer'sAdjustmentsandNameScore. These are where accuracy work happens, and accuracy work that had to preserve a signature would stop.Scorer,Scorer::WeightsandNormalizerare public because a host tunes and calls them; the machinery underneath them is not. HttpClient,FetcherandPayloadCacheas classes. Their errors are public, because a host rescues those, but nothing else about how bytes are obtained is. A host that wants to control fetching does it throughConfiguration.- Every
MEMBERSlist. They are the serialization order of a value object, read byto_hand by the snapshot checksum. They look like an enumeration of a record's fields and they are not one. - The per-adapter
Recordclasses and column constants. Nothing underSources::OfacSdn::Recordor its siblings is public: those track what a government publishes, and a publisher changing a column is exactly the change that must not require a major version here. - The parser toolkits' readers, backends and workbooks. The toolkits themselves are public — they are how an adapter is written — but the classes that do the reading underneath them are not.
- Anything under
spec/,benchmark/,canary/orbin/. None of it ships in the gem. The two conformance groups used to be the exception in spirit — expected to be run by adapter authors who had no way to get them — and they are now underlib/active_sanction/testing/and public, which is what the section above is about.
The enumerated public surface
Every name below is promised under the rules above. Nothing else is.
The entry point, and configuration
ActiveSanction
ActiveSanction::VERSION
ActiveSanction::MATCHER_VERSION
ActiveSanction::Client
ActiveSanction::Client::CAPABILITIES
ActiveSanction::Configuration
ActiveSanction::Deprecation
ActiveSanction::Configuration::DEFAULT_CACHE_DIRNAME
ActiveSanction::Configuration::DEFAULT_CANDIDATE_LIMIT
ActiveSanction::Configuration::DEFAULT_DOCTOR_TOLERANCE
ActiveSanction::Configuration::DEFAULT_MAX_REDIRECTS
ActiveSanction::Configuration::DEFAULT_MAX_RETRIES
ActiveSanction::Configuration::DEFAULT_OPEN_TIMEOUT
ActiveSanction::Configuration::DEFAULT_READ_TIMEOUT
ActiveSanction::Configuration::DEFAULT_RETAIN_PAYLOADS
ActiveSanction::Configuration::DEFAULT_RETRY_BACKOFF
ActiveSanction::Configuration::DEFAULT_SCREENING_LIMIT
ActiveSanction::Configuration::DEFAULT_SCREENING_THRESHOLD
ActiveSanction::Configuration::DEFAULT_SOURCES
ActiveSanction::Configuration::DEFAULT_STALE_AFTER
ActiveSanction::Configuration::DEFAULT_STORAGE_DIRNAME
ActiveSanction::Configuration::DEFAULT_SYNC_CONCURRENCY
ActiveSanction::Configuration::DEFAULT_USER_AGENT
ActiveSanction::Configuration::DEFAULT_XML_BACKEND
Instrumentation
ActiveSanction::Instrumentation
ActiveSanction::Instrumentation::EVENTS
ActiveSanction::Instrumentation::Event
ActiveSanction::Instrumentation::Notifications
ActiveSanction::Instrumentation::Notifications::NAMESPACE
The canonical record
ActiveSanction::Entity
ActiveSanction::Entity::TYPES
ActiveSanction::Name
ActiveSanction::Name::KINDS
ActiveSanction::Name::QUALITIES
ActiveSanction::Name::SCRIPTS
ActiveSanction::PartialDate
ActiveSanction::PartialDate::PRECISIONS
ActiveSanction::Address
ActiveSanction::Identifier
ActiveSanction::Identifier::KINDS
ActiveSanction::Snapshot
ActiveSanction::Subject
Screening
ActiveSanction::Matcher
ActiveSanction::Query
ActiveSanction::MatchResult
ActiveSanction::Normalizer
ActiveSanction::Normalizer::Form
ActiveSanction::Normalizer::Dictionary
ActiveSanction::Scorer
ActiveSanction::Scorer::Subject
ActiveSanction::Scorer::Result
ActiveSanction::Scorer::Reason
ActiveSanction::Scorer::Weights
ActiveSanction::Scorer::Weights::DEFAULTS
ActiveSanction::Country
Sources, and the toolkits an adapter is written with
ActiveSanction::Sources
ActiveSanction::Sources::Base
ActiveSanction::Sources::Definition
ActiveSanction::Sources::Remarks
ActiveSanction::Sources::OfacSdn
ActiveSanction::Sources::OfacConsolidated
ActiveSanction::Sources::UnConsolidated
ActiveSanction::Sources::CanadaSema
ActiveSanction::Sources::EuFsf
ActiveSanction::Sources::UkSanctionsList
ActiveSanction::Sources::AustraliaDfat
ActiveSanction::Parsers
ActiveSanction::Parsers::DelimitedTable
ActiveSanction::Parsers::DelimitedTable::Row
ActiveSanction::Parsers::XmlRecords
ActiveSanction::Parsers::XmlRecords::Record
ActiveSanction::Parsers::Spreadsheet
ActiveSanction::Parsers::Spreadsheet::Row
ActiveSanction::Parsers::Join
ActiveSanction::Parsers::Warning
ActiveSanction::Parsers::ColumnShape
Testing your own adapter
ActiveSanction::Testing
ActiveSanction::Testing::DEFAULT_FIXTURE_ROOT
ActiveSanction::Testing::StorageAdapterDefaults
Loaded by require "active_sanction/testing", never by require "active_sanction". The two shared example group names — "a sanction source" and "a storage adapter" — are public on the same terms as the
constants: a group is not renamed or removed within a major version, and the
options it accepts do not change meaning under an adapter that passes it
today.
New examples may be added to a group, and that is not a breaking change
even though it can turn a passing adapter red. It is the same promise the
extension points make read from the other side: what the group checks is what
Sources::Base and Storage::Base required all along, and an adapter that
fails a newly added example was always violating the contract — the group
merely started saying so. Additions land in a minor, with a CHANGELOG.md
entry naming them.
Storage, and what a fetch remembers
ActiveSanction::Storage
ActiveSanction::Storage::Base
ActiveSanction::Storage::Memory
ActiveSanction::Storage::FileSystem
ActiveSanction::Storage::ActiveRecord
ActiveSanction::Storage::Meta
ActiveSanction::Validators
ActiveSanction::ValidatorStore
ActiveSanction::ValidatorStore::FileSystem
ActiveSanction::ValidatorStore::Memory
ActiveSanction::Snapshot::Bundle
ActiveSanction::Snapshot::Bundle::Header
Operations
ActiveSanction::Sync
ActiveSanction::Sync::Report
ActiveSanction::Sync::Result
ActiveSanction::Sync::Result::STATUSES
ActiveSanction::Diff
ActiveSanction::Diff::Change
ActiveSanction::Rescreen
ActiveSanction::Rescreen::Alert
ActiveSanction::Rescreen::Alert::CHANGES
ActiveSanction::Doctor
ActiveSanction::Doctor::Report
ActiveSanction::Doctor::Diagnosis
ActiveSanction::Doctor::Diagnosis::STATUSES
ActiveSanction::Doctor::Finding
ActiveSanction::Doctor::Finding::SEVERITIES
ActiveSanction::Doctor::Checkup
ActiveSanction::Doctor::Profile
The error hierarchy
ActiveSanction::Error
ActiveSanction::ConfigurationError
ActiveSanction::SourceError
ActiveSanction::FetchError
ActiveSanction::ParseError
ActiveSanction::IntegrityError
ActiveSanction::StorageError
ActiveSanction::UnsupportedError
ActiveSanction::InvalidArgument
ActiveSanction::QueryError
ActiveSanction::MissingKey
ActiveSanction::Sources::DeclarationError
ActiveSanction::Sources::DuplicateKey
ActiveSanction::Sources::MissingPayload
ActiveSanction::Sources::UnknownSource
ActiveSanction::Parsers::ParseError
ActiveSanction::Parsers::XmlRecords::MalformedDocument
ActiveSanction::Storage::CorruptSnapshot
ActiveSanction::Storage::MissingSnapshot
ActiveSanction::Storage::UnsupportedSchema
ActiveSanction::ValidatorStore::CorruptStore
ActiveSanction::Snapshot::ChecksumMismatch
ActiveSanction::Snapshot::Bundle::Corrupt
ActiveSanction::Snapshot::Bundle::UntrustedSignature
ActiveSanction::Snapshot::Bundle::Unsigned
ActiveSanction::Snapshot::Bundle::UnsupportedFormat
ActiveSanction::HttpClient::Error
ActiveSanction::HttpClient::ConnectionError
ActiveSanction::HttpClient::TimeoutError
ActiveSanction::HttpClient::ResponseError
ActiveSanction::HttpClient::InvalidRedirect
ActiveSanction::HttpClient::RedirectLoop
ActiveSanction::HttpClient::TooManyRedirects
ActiveSanction::PayloadCache::CorruptEntry
ActiveSanction::PayloadCache::ChecksumMismatch
ActiveSanction::PayloadCache::PayloadMissing
ActiveSanction::Matcher::NotSynced
ActiveSanction::Sync::Failed
Also public, and not a constant
- Every public instance and class method on the types above, and their
documented keyword arguments. A method that YARD marks
@api privateis not, wherever it happens to live. - The module-level shorthand —
ActiveSanction.screen,.sync!,.diff,.rescreen,.doctor,.export,.import,.configure,.config,.client,.storage,.matcher,.with_configuration,.reset!,.reload!— which is the documented quickstart and is the same API asClient. - The Rails install generator as a command —
rails generate active_sanction:install— and the schema of the migration it writes. The generator class is marked private, because nothing outside Rails' own generator lookup refers to it by name. - The source keys —
:ofac_sdn,:ofac_consolidated,:un_consolidated,:canada_sema,:eu_fsf,:uk_sanctions_list,:australia_dfat. A key names a list in configuration, in a snapshot and in a stored audit record, so it does not change once published.