Skip to content

feat(compliance): documented allowlist-screen exceptions (waivers) - #134

Merged
eaitbrahim merged 2 commits into
mainfrom
feat/screen-exceptions
Jul 23, 2026
Merged

feat(compliance): documented allowlist-screen exceptions (waivers)#134
eaitbrahim merged 2 commits into
mainfrom
feat/screen-exceptions

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

What & why

Adds a general, per-asset / per-criterion allowlist-screen exception mechanism, plus keel assets exempt / unexempt to record and revoke one.

Motivating case: PAXG passes shariah (ayn, KB §65.5/§72.4/§86) and liquidity screening but fails the 4-year daily-history floor (441 on-chain -USD bars < 1460). Gold has centuries of off-chain price history and PAXG is a thin tokenised wrapper, so a human wants to record a documented, auditable exception that waives only the history criterion — never a silent exemption.

Design — honest and tightly bounded

  • New table screen_exceptions (asset, criterion, rationale, granted_by, granted_at; PK (asset, criterion)). Schema v8→v9 via a documented no-op migration (the table DDL is in the schema block; migrate() creates it on existing DBs before the version loop — mirrors the v6/v7 attestation/profile pattern).
  • WAIVABLE_CRITERIA = frozenset({"history"}) — only a DATA/market criterion may ever be waived. The shariah core (missing attestation, haram_sector, riba_yield, dayn/unknown backing) and settlement can never be waived. Enforced two ways: the CLI --criterion is a click.Choice restricted to this set, and screen_asset only reads a waiver inside the history branch (structural isolation) and checks membership — belt-and-suspenders.
  • Surfaced loudly, never silent: a waived history failure becomes a ! ... WAIVED by documented exception: <rationale> warning and the verdict flips to ADMIT. Self-retiring: the waiver is only consulted when the check would otherwise fail, so once -USD bars ≥ 1460 it becomes inert automatically.
  • Ungated like assets attest (an exception cannot itself place an order; the 17 guards.py rails still enforce every trade). Revocable via unexempt (a de-risking action). Shown in assets list.

Tests (TDD, written first)

  • screen: waiver admits + warns loudly; self-retires when bars ≥ floor; a waiver for a non-waivable criterion fails closed; a waiver rescues only history (other failures still REJECT).
  • repository: upsert/get/list/delete round-trip, ON CONFLICT update, asset-scoping.
  • db: fresh + upgraded DBs reach v9 with the table; migrate idempotent.
  • cli: exemptscreen ADMITs with WAIVED; bad --criterion rejected; list shows it; unexempt reverts to REJECT.
  • Mutation-checked the WAIVABLE_CRITERIA guard (emptying it fails the positive-path tests).

uv run pytest -q1399 passed; uv run ruff check → clean.

🤖 Generated with Claude Code

eaitbrahim and others added 2 commits July 23, 2026 10:11
Adds a general, per-asset per-criterion exception mechanism so a human can
record a DOCUMENTED, auditable waiver of an admission criterion -- the
motivating case being PAXG's 441 daily bars falling short of the 4-year
history floor while shariah/liquidity screening pass clean.

The set of criteria that may EVER be waived is restricted to
WAIVABLE_CRITERIA (currently just `history`): the shariah checks
(attestation, haram_sector, riba_yield, dayn/unknown backing) and
`settlement` are never consulted for a waiver, both by construction
(screen_asset only reads `waived` inside the history branch) and by an
explicit WAIVABLE_CRITERIA membership guard as defense in depth. A waiver
is surfaced as a loud warning (never a silent pass) and is self-retiring:
once bars clear the floor, screen_asset emits nothing about it.

- keel/data/db.py: `screen_exceptions` table, SCHEMA_VERSION 8 -> 9, a
  documented no-op v9 migration (table creation is handled by the additive
  `_SCHEMA_STATEMENTS` pass, same pattern as v6/v7).
- keel/data/repository.py: upsert/get/list/delete_screen_exception.
- keel/compliance/screen.py: `screen_asset(..., waived=None)` and
  `WAIVABLE_CRITERIA`.
- keel/cli.py: `assets exempt`/`assets unexempt` (Choice-restricted to
  WAIVABLE_CRITERIA), `assets screen` now loads and passes waivers, and
  `assets list` prints a `exceptions:` section when any are recorded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial review of #134 found 2 should-fix + 2 nits:

- WAIVABLE_CRITERIA guard was tautological (`"history" in WAIVABLE_CRITERIA`
  is a compile-time True) and its safety test was vacuous (default bars=2000
  never entered the history branch, so `waived` was never consulted).
  Replaced the inline check with an up-front `effective_waived` filter in
  `screen_asset` -- real defense-in-depth against a future WAIVABLE_CRITERIA
  shrink or a careless new branch -- and rewrote the safety test to actually
  enter the history branch, plus added a test that a stray non-waivable key
  riding alongside a real waiver is dropped, not honored.

- A blank/whitespace rationale used to ADMIT with an empty "WAIVED by
  documented exception: " warning -- an undocumented "documented exception."
  `screen_asset` now only honors a waiver whose rationale is non-blank
  (mirrors the existing unsourced-attestation guard); `assets exempt`
  rejects a blank `--rationale` at the CLI boundary too.

- `assets exempt`/`assets unexempt` now uppercase `--asset`, matching the
  uppercase asset code `_screen_product` looks waivers up by -- a
  `--asset paxg` waiver no longer silently no-ops against `PAXG-USD`.

- `delete_screen_exception` now returns the rowcount removed; `assets
  unexempt` only echoes a revoke confirmation when a row actually existed,
  otherwise reports "no such exception" instead of a false success.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@eaitbrahim
eaitbrahim merged commit e7663ed into main Jul 23, 2026
1 check passed
@eaitbrahim
eaitbrahim deleted the feat/screen-exceptions branch July 23, 2026 14:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant