# Save the reviewed note, not a stale decision

A financial-services FDE proof lab · about 50 minutes · Python 3.11+ standard library.

A case handler approves an internal note that says a temporary credit was recorded. Before your application saves it, another worker updates the case. Your note passed review, but does that review still support writing it now?

This workshop turns that gap into running code. You use two connections to a disposable SQLite database, inspect actual committed rows, deliberately move one check to the wrong place and rehearse the receiving decision. All records are invented. No banking account, model, network call or customer data is involved.

## 1. Understand the work before the transaction

A debit-dispute case records an investigation. A provisional credit is temporary; it is not the final outcome. The note in this exercise records one observed credit state for internal case operations. Saving it does not transfer money, determine liability, contact a customer or close the investigation.

In the previous [pagination lab](/training/guides/financial-extract), you established a complete source snapshot. Completeness and freshness are different: a coherent snapshot can become old before a write. Here, the case row and the note live in one local database so you can study that exact boundary.

A case revision identifies a version of the case. A source revision identifies the version of the supporting observation. The fictional review binds both revisions and the exact requested statement. Think of reviewing a particular Git diff: approval of one diff does not approve a different patch merely because it uses the same branch name.

## 2. Give the database words a concrete meaning

| Term | What it means here | What to inspect |
| --- | --- | --- |
| Transaction | A boundary around the read, decision and write | BEGIN, comparison, INSERT and COMMIT in the trace |
| Commit | Make the note durable in this scratch database | A second connection can read the stored row |
| Rollback | Discard this transaction's unfinished changes | The injected failure leaves no note |
| Contention | Two connections want the write boundary at the same time | The second receives SQLite's busy result |
| Idempotency key | The identity of one intended operation | The unique pair of workspace and operation ID |
| Receipt | The committed result of that operation | Exact statement and versions from the earlier save |

The safe writer acquires `BEGIN IMMEDIATE`, checks for an existing operation and then reloads the case before inserting a new note. The local database permits one writer at a time. It is the placement of the read inside that boundary that matters; writing `BEGIN` somewhere in the function does not make an earlier read current.

The deliberate `--check-before-lock` mode moves only the case read outside the boundary. The review, wording comparison, operation uniqueness and rollback remain in place. This isolates a timing defect without making every check disappear.

## 3. Run the transaction proof lab

Download `fde-financial-handoff.py` into a new folder. It needs Python 3.11 or later; check `python3 --version` first and use your installed 3.11+ interpreter if `python3` points to an older version. The script creates and removes its own temporary databases. The reports below are files you choose to keep.

```bash
python3 fde-financial-handoff.py
python3 fde-financial-handoff.py --json > handoff-guarded.json
python3 fde-financial-handoff.py --check-before-lock --json > handoff-before-lock.json
```

Before opening the reports, predict which cases save a new row and which should return no new write.

| Case | Evidence to compare | Question to answer |
| --- | --- | --- |
| supported-note | Review, source revisions and committed row | What exactly was saved? |
| source-changed-before-write | B's committed update before A's transaction | Can the old approval support a new note? |
| changed-reviewed-content | Proposed statement versus accepted request | Does approval still match the sentence? |
| identical-retry-after-source-change | Earlier receipt versus current source | Is the receipt history or a fresh observation? |
| changed-operation | Same operation ID, different request | Should a new intent replace the old result? |
| rollback-and-retry | Rows after the injected failure and after retry | Did an uncommitted insert leave a durable note? |
| overlapping-deliveries | Chronological timeline and both connections | Did the second writer actually encounter a lock? |

Each attempt contains its exact request and review, source and note rows before and after, an outcome, the checks actually reached and any historical receipt. The top-level `timeline` is chronological. In the overlap case, connection B's first attempt happens *inside* A's insert hook; the attempts list groups the complete A result before B's busy result. Use the timeline to understand their actual order.

Inspect `source-changed-before-write` in both reports. The source and review are unchanged between runs. Explain why using a captured revision lets the broken writer save an old statement even though it starts a transaction later. Do not repair this by silently changing the sentence to final credit: that sentence has not been reviewed.

## 4. Follow two deliveries through one database

The overlap case uses two real SQLite connections to the same file. A inserts while holding its transaction. Its hook invokes B, which attempts `BEGIN IMMEDIATE` with a zero lock-wait timeout. The database returns busy. A then commits; the caller makes one bounded retry through B and reads the stored receipt. There are no sleeps or fabricated lock outcomes.

```text
Connection A: acquire → inspect → INSERT ───────────→ COMMIT
Connection B:                         try → BUSY              retry → receipt
Committed notes: 0                    0              1                  1
```

This controlled ordering is an experiment, not a load test. It establishes what happens when these two transaction lifetimes overlap. It does not measure throughput, fairness, process crashes or every scheduling order. In production, bound retries by time and count, apply an appropriate backoff policy and preserve the original operation identity. Do not retry every error indefinitely.

For rollback, the injected exception happens after INSERT and before COMMIT. The script explicitly rolls back and then retries from the second connection. Read the actual rows after the failure. An exception message by itself is not proof that storage returned to the expected state.

## 5. Change an input and prove a new boundary

Importing the file does not run the scenarios. `write_note` is the small decision boundary; `approved` creates an explicitly trusted review fixture. Never expose that fixture helper as an endpoint that lets generated content approve itself.

In a copy of the lab, change only `source_revision` in connection B before the write. Leave the case revision and wording unchanged. Predict whether the original review is still sufficient, then inspect the guarded output. Next change only the case revision. Each version has an independent meaning.

Save this as `source-only.py` beside the lab and run it with your Python 3.11+ interpreter. It exercises a new ordering directly through the same writer, using a separate temporary database.

```python
import runpy
from copy import deepcopy
from pathlib import Path
from tempfile import TemporaryDirectory

lab = runpy.run_path("fde-financial-handoff.py")
with TemporaryDirectory() as folder:
    path = Path(folder) / "source-only.db"
    a, b = lab["connect"](path), lab["connect"](path)
    try:
        lab["initialize"](a)
        request = deepcopy(lab["REQUEST"])
        review = lab["approved"](request)
        def change_only_the_source():
            b.execute("UPDATE cases SET source_revision=2")
        result = lab["write_note"](a, request, review,
                                   before_transaction=change_only_the_source)
        assert result["outcome"] == "stale_review"
        assert lab["notes"](b) == []
        assert lab["source"](b, request)["case_revision"] == 1
        print("Changed source, unchanged case: no stale note committed.")
    finally:
        a.close()
        b.close()
```

Try a review for a different case, or replace `case_operations` with an unrelated role. Inspect the stopping reason and prove no note was added. The trusted workspace is a separate input from the request's workspace; change the request and prove the scope check still runs even for a retry.

Finally, modify the overlap hook so A rolls back after B receives busy. What should B's retry do when no receipt exists? Show the rows and timeline. A valid retry may now create the one note instead of returning a receipt. Keep that distinction in the receiving explanation.

## 6. Hand over evidence a colleague can use

Prepare a short review with a peer or your existing facilitator. If working alone, mark the exercise self-rehearsed. This workshop does not book mentorship or certify an assessment.

Bring both reports and one unfamiliar fixture. Ask the receiver to identify the stopping boundary without seeing your explanation first. Then ask them to distinguish three tasks: refresh stale review with case operations, retry a busy delivery within policy, and investigate a reused operation key with changed content. Record any assistance they needed and the exact repair requested.

Keep the receipt's historical statement and revisions visible. A returned receipt says the earlier operation committed; it does not say the current case still has temporary credit. A new business decision needs fresh evidence and a separately identified, reviewed operation.

Before connecting a real service, document the unproven boundaries: authenticated identity and reviewer authority; atomicity across the actual source and destination; source version semantics; the target database's isolation and retry behavior; durable recovery after process failure; and any external delivery receipt. A SQLite lock cannot protect a banking API that lives outside the transaction. Do not hold the transaction open for a model call or a human decision. For a remote source, agree a supported version-precondition or handoff protocol with its owner and test that contract separately.

## 7. Read the primary references

Technical references checked 23 October 2026:

- [SQLite transaction control](https://www.sqlite.org/lang_transaction.html): explicit write transactions, commit, rollback and busy behavior.
- [SQLite isolation between connections](https://www.sqlite.org/isolation.html): what separate connections observe.
- [CFPB: unauthorized transactions and temporary credit](https://www.consumerfinance.gov/ask-cfpb/how-do-i-get-my-money-back-after-i-discover-an-unauthorized-transaction-or-money-missing-from-my-bank-account-en-1017/): background for the business distinction. This exercise does not calculate legal deadlines or decide consumer rights.

Continue with the [receiving review sheet](/training/guides/receiving-review), carrying the actual rows, timelines and unresolved integration questions.
