Decoded the vocabulary from the binary rather than trusting a case count: FUN_18012ec50 is `cmp ecx,0x1d` plus a 30-entry jump table at 0x18012ed9c, each case `mov ecx,<atom>; jmp <atom->string>`. Resolving those atoms against fut_atoms.tsv yields the exact token list, and it matches club_type_filter one-for-one — 30 implemented, none missing, none invented. That is worth a test rather than a note. A MISSING arm answers a real tab with unsupported_type and an empty screen; an INVENTED arm is worse, because it is dead code that looks like coverage. Mutation-checked: renaming the leaguelogos arm fails the test. Two facts fall out that were previously guesswork. There is no `playergoalkeeper` token — the client has only DEF/MID/FWD tabs — so a goalkeeper appearing under playerdefender is CORRECT and not a filter bug, which I had flagged as suspicious while sweeping. And `healing`/`contract`/`training` exist as ?type= arms even though consumables have their own route. Also completes the last unapplied item of plan section 7: the full vocabulary is now written into ENDPOINT_MAP.md with how it was derived.
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 …/clubandPUT|POST …/user/clubatomically update the shared account JSON. Every input returns the required zero-atom200 {}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 versionedOPENFUT_FIFA17_CATALOG. An unmapped definition is dropped and counted, never faked. - Instance identity (
id) — a stable, persistent, reversible wire integer from the genericopenfut-identitystore under the FIFA 17 wire-id policy (monotonic from100_000_001). The same owned instance keeps its id across restart and reverses exactly; two copies of one definition share aresourceIdbut get distinctids. 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.