# Matching, lookup input, and analytics verification

Verified in `/home/stevan/dev/compass-verify-matching` on
`fix/review-20260906-verify-matching`, starting from clean commit
`d6a057f9f8632c2ff7eff67fa7fa4ed013ce626a`.

Fix commit: **`48dbd6772` — [COMPASS] Recover legacy lookups and explain gap blockers accurately**.
Parent reviewed the production diff and integrated this as **`b3e193539`**.
This is a local commit covering 18 owned files. No other worktree was edited, no
migration was added, and no application deployment or push was performed.

## Findings

| ID | Outcome | Evidence and final behavior |
|---|---|---|
| #5 | PASS | `AccountCapability#normalize_wildcards` normalizes blank currency/IBAN country; `EntityRestriction#normalize_wildcards` normalizes blank client country before validation and uniqueness checks. Model and request suites exercise browser empty-string payloads for creation and updates, including both wildcards. Nil remains the matching wildcard. |
| #8 | PASS | `MatchProviders::ForwardMatching#check_forward_type` and `MatchProspects::ForwardEvidence#forward_contradiction` both require deliverable OR NDF when Either is requested. Specs exercise deliverable-only, NDF-only, both, neither, and the distinction between an omitted prospect and a qualifying prospect carrying a negative forward limitation. |
| #13 | FIXED | The existing partner group aggregation is correct under the accepted rule: green with at least one supported member and no recorded unsupported member, red with any unsupported member, grey with no member recorded. Specific entity rules override general rules separately for every member; prospect evidence retains every effective restriction. The new fix preserves `blocked_members` and distinct `blocked_restrictions` in both admin/API snapshots, so subsequent analytics retains that distinction. Request coverage proves two blocked member rules remain two blockers and one general rule remains one rule. Commit `48dbd6772`. |
| #16 | PASS | Browser JSON parser rejects malformed JSON, JSON null, null elements, scalar and object shapes without creating or matching. Accepted blank browser text and omitted input still become `[]`. API raw JSON shape preservation catches Rails deep-munged null elements and preserves strong parameter filtering for valid rows; explicit API null is rejected. Model duplicate validation skips invalid elements safely. |
| #17 | FIXED | Existing model validation correctly removes blanks/duplicates and rejects empty arrays, unknown currency codes and invalid nested account currencies before persistence. New six-route regression proved saved historical blank-currency lookups still caused 500s on admin/API show, CSV and PDF. `MatchProviders::InvalidLookupError` now takes admins to the existing prefilled correction form and yields 422 JSON for API reads. Saved attributes, result snapshots and record count remain unchanged; no fabricated matches or migration. Commit `48dbd6772`. |
| #18 | PASS | Client country is required at the model boundary and API omission returns 422 without inserting a lookup. Country/group validation accepts only the configured ISO/group vocabulary. |
| #19 | PASS | Prospect direct currency evidence now requires a supported requested sell/buy pair allowed by the buy corridor. GBP→USD with USD restricted to JPY cannot rank as direct; specific/both direction precedence and client category gates remain intact. Restrictions remain visible and contradictory requested-pair restrictions cap the tier. Existing corridor evidence suite passes. |
| #20 | PASS | Empty browser industry normalizes to nil and `IndustryMatching#skip_industry?` treats blank as not requested; request coverage compares stored values and matching output against omission. |
| #21 | FIXED | Existing two-account and multi-exclusion conjunction checks passed, but broader verification found additional false explanations. New analytics treats red currency pairs as alternatives because live matching accepts amber as soon as any requested pair turns green. A real matcher regression demonstrates this before/after behavior. Currency alternatives remain conjunctive with independent account/entity/exclusion requirements. New snapshots count distinct effective group entity rules: two specific rules require both fixes, while one general restriction counts once regardless of the number of affected members. Historical group entity snapshots lacking the rule list use `:incomplete`, `Recorded blockers — rule details unavailable`, and no closest-miss promise. Distinct exclusion direction rules are retained even when their reason text is identical; one `both` rule encountered twice is still counted once. CSV now retains AND/OR headings for multi-label gaps and incomplete framing. Commit `48dbd6772`. |
| #22 | FIXED | Existing DE client / GB sender request and service tests verify Germany in labels, distinct gap keys, and CSV. Added missing EU and SEPA display labels; a helper invariant now covers every accepted country group. Blocked jurisdiction member labels take precedence over the whole group label when stored. Commit `48dbd6772`. |
| #24 | PASS | Authenticated admin snapshot route supports owner, colleague, and API lookups; renders stored results with source details; rejects deleted records and unauthenticated access. Request tests prevent calls to both live matchers, assert no form/rerun/export controls, and compare all saved attributes before/after. The live lookup controller remains owner scoped. |
| #33 | PASS | Prospect evidence is preloaded once per gap coverage calculation and reused across gaps/lookups; last-contact work is disabled. Existing query-bound and matching-equivalence specs pass. Independent uncached runtime measurements below confirm fixed query count as both demand and evidence grow. No cache was added. |

## Country constants adjacent to #13

Inspected original fix `c29a004e0`. Runtime checks confirm Greece uses `GR` exactly
once in the EU group, `EL` appears in no country group, and every member of all
eight configured groups is recognized by `CountryValidator`.

All 14 added codes are recognized: `AD BJ BN BF ER GW VA MR PG CG SM ST TL TM`.
These additions fill the existing groups' missing country vocabulary; group
membership was not otherwise changed. Country and country-or-group validator
specs are included in the final focused run.

## Reproduction and validation

All database work used `DISABLE_SPRING=1 RAILS_ENV=test TEST_ENV_NUMBER=98`, with
`DATABASE_URL` removed and a synthetic `DEVISE_SECRET_KEY`. Created and loaded
schema into **`currencytransfer_test98`**. No shared/default test database was used.

- Baseline: **744 examples, 0 failures**, seed **14833**, 48.73 seconds execution
  (`/tmp/compass-matching-baseline.log`).
- Legacy-route regression before fix: **6 examples, 6 failures**, all actual
  HTTP 500 responses, seed **13596** (`/tmp/compass-legacy-red.log`). After fix:
  **6 examples, 0 failures**, seed **33895** (`/tmp/compass-legacy-green.log`).
- Entity/currency analytics regression before fix: **9 failures**, seed **14075**
  (`/tmp/compass-analytics-red.log`). The subsequent expanded affected run passed
  **161 examples, 0 failures**, seed **47852** (`/tmp/compass-analytics-green.log`).
- Adjacent regressions: **102 examples, 4 failures** before fixes, seed **41211**:
  lost CSV conjunction, EU/SEPA labels, and merged exclusion direction rules
  (`/tmp/compass-adjacent-red.log`).
- First expanded run: **919 examples, 1 failure**, seed **48595**. The failure was
  another existing spec explicitly expecting the old country+reason exclusion
  collapse. Updated it to retain both independent direction rules, consistent
  with the reproduced defect (`/tmp/compass-matching-final.log`).
- Final expanded focused run: **919 examples, 0 failures**, seed **40475**,
  **48.61 seconds** execution plus **7.46 seconds** load
  (`/tmp/compass-matching-verified.log`). It includes model validation, both
  matchers, group jurisdictions/exclusions, unserved analysis, gap prospects and
  coverage, admin lookups/analytics/capability requests, all API lookup request
  suites, helper tests, and relevant controller contracts. Parent owns the full
  integrated suite.
- `git diff --check` passes. RuboCop on 16 changed Ruby files reports **zero
  changed-line offenses**, with **85 pre-existing offenses** elsewhere in those
  files. Baseline/current comparison also confirms both modified analytics and
  entity service methods are offense-free. Output:
  `/tmp/compass-matching-rubocop-final.json`.
- Reviewed the complete owned diff, simplified the new classification branches,
  and kept response ownership, snapshot immutability, historical compatibility,
  and accepted matcher defaults intact. No additional agents were spawned.

## #33 independent runtime measurement

Synthetic fixture with every prospect evidence association populated, including
provenance, contacts, and conversations. Run inside a transaction and rolled back.
Uncached SQL was counted with `sql.active_record`, allocations with
`GC.stat(:total_allocated_objects)`, and elapsed time with the monotonic clock.

| Scenario | Uncached queries | Allocated objects | Elapsed ms | Recency queries |
|---|---:|---:|---:|---:|
| Current coverage: 1 lookup / 1 prospect | 16 | 8,798 | 21.23 | 0 |
| Current coverage: 20 lookups / 10 prospects | 16 | 60,178 | 85.39 | 0 |
| Match 20 × 10 with evidence already loaded | 0 | 43,696 | 56.95 | 0 |
| Comparison: fresh normal matcher for each of 20 lookups / 10 prospects | 480 | 559,046 | 1,209.92 | 40 |

This proves evidence reuse and removal of recency queries; it does not claim
constant runtime or allocation cost. In-memory matching still scales with
lookup count and evidence volume. These are single local synthetic measurements,
not production benchmarks. The repeated-loading comparison exercises the current
normal matcher with default recency enabled; it is not a checkout of an earlier
release.

Reproduction script: `/tmp/compass-verify-20260906/profile_gap_coverage.rb`.
Raw output: `/tmp/compass-gap-profile.log`.

## Remaining limits

No fixable validated defect in the assigned findings is left deferred. Older
snapshots cannot recover entity rule details that were never stored; the code now
states that uncertainty without recomputing or rewriting historical results.
The 919-example run is focused coverage, not the full application suite, a new
live-browser audit, or production-load testing. No external APIs were called.
