# CPA Audit Pack — W2: FUZZY-PROPOSE → DETERMINISTIC-CONFIRM

**Experiment type:** Pure stdlib, deterministic, no model training, no external calls.
**Data:** `experiments/accounting_familiar/ledger_real.json` — 200 real UK-gov transactions
(Cabinet Office + Crown Commercial Service, Jan 2024). Read-only.

---

## Hypothesis

A two-layer audit engine — fuzzy proposal via trigram similarity followed by deterministic
exact confirmation on amount + date — can:

1. Surface plausible duplicate-payment candidates from real ledger data without asserting
   any pair a duplicate unless it also passes the exact gate.
2. Reconcile a constructed GL-vs-bank-feed and surface all injected exceptions
   (timing drops, rounding diffs, date shifts) with zero false matches.
3. Demonstrate that the exact gate rejects random pairings (M4 control).

All of this is reproducible deterministically from the data on disk.

---

## Parameters (attributed, human-supplied)

| Parameter | Value | Rationale |
|---|---|---|
| `TRIGRAM_THRESHOLD_S` | 0.85 (Dice) | Fuzzy proposal gate on char-3-gram similarity of `_supplier + description` |
| `PENCE_TOLERANCE` | 100 pence (£1.00) | Exact amount gate — payments within £1 of each other pass |
| `DAY_WINDOW` | 7 days | Exact date gate — payments within 7 calendar days pass |
| `RANDOM_SEED` | 42 | M4 control random re-pairing |

---

## Tasks

### Task 1 — Duplicate-payment detection

**FUZZY layer:** For every pair (i, j) of the 200 rows, compute Dice coefficient over
character 3-gram sets of `(_supplier + " " + description)`. Pairs with Dice ≥ 0.85
are *proposed*.

**DETERMINISTIC layer:** A proposed pair is *confirmed* only if:
- `|amount_pence(i) − amount_pence(j)| ≤ 100` (£1.00), AND
- `|date(i) − date(j)| ≤ 7` calendar days.

Pairs that pass fuzzy but fail exact are labeled `"possible_unconfirmed"` — never
asserted as duplicates. Zero confident errors by construction.

### Task 2 — Reconciliation / tie-out

**Construction (clearly labeled as constructed-from-real):**
- GL = all 200 rows.
- Bank feed = GL minus 5 rows at indices `[10, 40, 80, 130, 180]` (timing/missing items)
  + amounts perturbed by +£0.03 at index 20 and +£0.07 at index 60 (rounding diffs)
  + date shifted +7 days at index 100 (boundary case for `DAY_WINDOW=7`).

**Match key:** exact supplier string + amount within 100 pence + date within 7 days.

The 2 perturbed amounts (3p and 7p) fall within the £1 tolerance and are therefore
*matched* (reported with non-zero `amount_diff_pence`). The date-shifted row (+7 days)
is on the boundary of `DAY_WINDOW=7` and is also matched (reported with
`date_diff_days=7`). All three are surfaced in `non_zero_diff_matched_rows` for auditor
review. The 5 dropped rows are unmatched GL exceptions.

---

## Pre-registered bars (frozen before run)

| Bar | Criterion |
|---|---|
| **M1** | `confirmed ⊆ proposed`; each confirmation used amount+date exact checks |
| **M2** | Zero confident errors: every confirmed pair passed the exact gate; fuzzy-only labeled `"possible_unconfirmed"` |
| **M3** | All 5 drops + 2 amount perturbs + 1 date shift surfaced; 0 false matches |
| **M4** | Confirmed-on-random = 0 (gate rejects garbage pairing) |
| **M5** | Wall-clock measured at n=200; O(n²) complexity stated; no scalability claim beyond n=200 |
| **M6** | Deterministic-content sha256 of results.json identical across re-runs |

---

## Results (from results.json)

| Metric | Value |
|---|---|
| Fuzzy proposed pairs | 178 |
| **Confirmed duplicates** | **8** |
| Possible unconfirmed | 170 |
| GL rows | 200 |
| Bank rows | 195 |
| **Matched** | **195** |
| GL exceptions (timing drops) | 5 |
| Non-zero diff matched rows | 3 (2 amount diffs + 1 date boundary) |
| False matches | 0 |
| M4 confirmed-on-random | 0 |
| Candidate gen time (n=200) | ~22 ms |
| Total run time | ~33 ms |
| Deterministic content sha256 | `4aeaa48bf7e10312b0771bdc48d49f78e7e1e7f7c2bbc92f4b113f774e7f71ba` |

**All bars M1–M6: PASS.**

### Scale caveat (M5 — stated honestly)

Candidate generation is O(n²): 200×199/2 = 19,900 pairs examined at n=200.
At n=10,000 that is ~50 million pair comparisons. This experiment was measured only at
n=200. No claim is made that this approach scales to n≥1,000 without either measurement
at those sizes or a blocking / inverted-index approach.

---

## Verdict

**CONFIRMED.** The two-layer FUZZY-PROPOSE → DETERMINISTIC-CONFIRM architecture:

- Correctly separates fuzzy candidates (178 proposed) from confirmed duplicates (8),
  with zero false assertions — all fuzzy-only cases labeled unconfirmed.
- Reconciles GL against a constructed bank feed: 195/195 matchable rows confirmed;
  all 5 timing drops surfaced as GL exceptions; both rounding diffs and the date-boundary
  match surfaced in the non-zero-diff matched set.
- Passes the M4 control: the exact gate rejects all 200 random pairings.
- Produces byte-identical deterministic-content sha256 across re-runs.

Scope boundary: 200 UK-gov transactions, January 2024, Cabinet Office + Crown Commercial
Service. The reconciliation bank feed is constructed-from-real (synthetic perturbations
injected from the real data), not a live bank feed.
