# openfut-utas-host

The FIFA 17 UTAS migration boundary. It accepts the client-visible HTTP surface,
serves migrated routes from Rust/Core plus host-owned durable stores, and proxies only
the unclassified tail to the Python behavioral oracle.

```
FIFA 17 ──HTTP──▶ openfut-utas-host
                    ├── migrated route ──▶ Rust adapter / Core / host stores
                    └── unclassified tail ──▶ Python UTAS oracle
```

`src/lib.rs::classify` is the route-level source of truth. The current Rust surface
includes club/squad/user reads, club rename, auth/session/client data, Store/economy,
packs, owned-item moves, market/trade-pile, and the observed hub support routes.

## Safety model

- Classification happens exactly once before execution. There is no "try Rust then
  Python"; a mutation cannot be double-applied.
- A route classified to Rust never falls back to Python on a Core/store/projection
  failure. Each handler uses its captured fail-closed or honest-empty wire contract.
- `PUT …/club` and `PUT|POST …/user/club` atomically update the shared account JSON.
  Every input returns the required zero-atom `200 {}` response; rejection and
  persistence failures remain visible in logs.
- Numeric `GET …/squad/<n>` returns the one Core-backed current squad, matching the
  Python oracle's single-current-squad behavior.
- Python remains the behavioral oracle and rollback backend for routes not yet
  classified to Rust. New economy behavior belongs in Rust/Core, never Python.

## Configuration (env)

| Var | Required | Default | Meaning |
|---|---|---|---|
| `OPENFUT_UTAS_HOST_ADDR` | yes | — | client-visible host listen address |
| `OPENFUT_UTAS_PYTHON_URL` | yes | — | Python oracle base for the unclassified tail; must differ from this host |
| `OPENFUT_FIFA17_CATALOG` | yes | — | FIFA 17 definition identity catalog |
| `OPENFUT_IDENTITY_STORE` | yes | — | persistent owned-instance ↔ wire-id store |
| `OPENFUT_PERSONA_ID` | yes | — | non-zero FIFA persona id shared by LSX/Blaze/POW/UTAS |
| `OPENFUT_MARKET_DB` | yes | — | durable host-owned transfer-market SQLite DB |
| `OPENFUT_PILE_DB` | yes | — | durable host-owned item-pile SQLite DB |
| `OPENFUT_CORE_URL` | no | `http://127.0.0.1:8080` | OpenFUT Core base |
| `OPENFUT_FIFA17_TABLES_DIR` | no | `fifa17-recon/data/tables` | FIFA entity tables |
| `OPENFUT_CLIENTDATA_DB` | no | identity-store sibling `clientdata.json` | durable opaque client-data JSON |
| `OPENFUT_ACCOUNT_PATH` | no | `FUT_ACCOUNT_PATH`, then identity-store sibling `active_account.json` | shared FIFA account/club JSON |

Startup fails if required identity or durable economy state cannot be opened. No
placeholder production identity source is substituted.

## Identity model (resolved)

FIFA renders an owned card by resolving `resourceId & 0xffffff` against the
client's **own local players table**; an invented id renders a **blank generic
card** (proven live — `fut_cards.py:11-21`). Two distinct identities, never
conflated, are resolved by [`Fifa17IdentityResolver`] (the single production path):

- **Definition identity** (`resourceId`/`assetId`) — the card's real FIFA asset
  id, from the versioned `OPENFUT_FIFA17_CATALOG`. An unmapped definition is
  **dropped and counted**, never faked.
- **Instance identity** (`id`) — a stable, persistent, reversible wire integer
  from the generic `openfut-identity` store under the FIFA 17 wire-id policy
  (monotonic from `100_000_001`). The same owned instance keeps its id across
  restart and reverses exactly; two copies of one definition share a
  `resourceId` but get distinct `id`s. The namespace is globally monotonic
  within `(fifa17, owned-item)` — no per-account column is needed because Core
  owned-instance ids are globally-unique UUIDs.

Production has a frozen post-P1 baseline and a hot Python rollback. A source change
passing local tests is **not** deployment approval. Build verification, staging, host
restart, and live-client promotion remain operator-gated; the current state and
promotion evidence live in the OpenFUT Obsidian vault.

Logs are safe by construction: no auth/session/device/token material — only owner,
route, filter summary, counts, status.
