Files
OpenFUT/openfut-utas-host/ROUTE_AUTHORITY.md
T
OpenFUT Agent 0e2ca5a7c3 docs(route-authority): record landed Store/Market/pack handlers (unrouted)
Update cutover progress: Store BUY/pack-open/quick-sell, market
list/query/cancel/buy + move-items, durable listing/pile stores, and the
pack-content generator are implemented + tested (4d2b8b9) but NOT routed.
Remaining before the single classifier barrier: sync<->async Server wiring +
classify() routes, host<->Core writer E2E, Python differential, host
concurrency matrix, importer restart/idempotency, then the barrier + no-fallback
proofs.
2026-08-13 20:48:39 +00:00

9.0 KiB

FIFA17 UTAS Route Authority (economy cutover gate)

Machine-auditable ownership of every FIFA17 UTAS route that touches the economy cluster. This is the deployment gate for the Rust economy cutover (R1/E1): before Rust economy authority is enabled, every row's Target must be reached and no Python (proxied) row may still write Core-owned state.

Cluster state = coins, owned inventory (items/purchased), unopened pack entitlements (unopenedPackIds). points has no writer (read-only). EASFC powFunds is a separate balance, out of cluster.

Legend: R = Rust/Core authoritative, P = Python proxied (oracle). Evidence lines refer to fifa17-recon/tools/{utas_server.py,fut_store.py}.

Writer routes (mutate cluster state)

Route Method Python handler Writes Current Target Core primitive
/ut/game/<sku>/match POST match_routerecord_match (fut_store 554) coins P R grant_reward
/store/transaction PUT store_buyopen_packspend (utas 3702) coins, purchased, packsOpened, nextItemId P R purchase_entitlement (+ redeem_entitlement)
/purchased POST purchased_itemsopen_pack+consume_unopened_pack+move_items (utas 3716) coins, purchased, unopenedPackIds, items, nextItemId P R redeem_entitlement
/ut/game/<sku>/item/<id> DELETE quick_sell_url_routequick_sell (utas 1234) coins, items, purchased P R sell_item
/ut/delete/game/<sku>/item POST quick_sell_routequick_sell (utas 1273) coins, items, purchased P R sell_item
/ut/game/<sku>/item PUT item_routemove_items (utas 1342) items, purchased P R redeem_entitlement/move (inventory-only)
/ut/game/<sku>/trade/<id> POST/PUT trade_route buy-now spend+add_items (utas 3895/3899) coins, items, nextItemId P R purchase_item (synthetic-seller mint)
/auctionhouse,/transfermarket POST auctionhouse_routelist_for_sale (utas 3865) listings, nextListingSeq P R listing-state (see note)
/ut/delete/game/<sku>/trade/<id> DELETE delete_trade_routeremove_listing (utas 3935) listings P R listing-state (see note)
/ut/game/<sku>/squad PUT squad_routesave_squad (utas 3430) squads (item refs) R (SquadReplace→Core) R Core squad tx (already migrated)

Note (market listings): listings/nextListingSeq are the user's own sale pile; the buyable auction inventory is synthetic (PACK_POOL-derived, not persisted). There is no sale-credit, expiry-return, or fee (audit §5). Listing/cancel move no coins and no ownership, so they are low-risk; a minimal durable listing store (or keeping the synthetic-only model) is the market slice's only decision.

Reader routes (emit cluster state; go STALE if Rust writes while these read Python)

Route Method Python handler Reads Current Target
/user/credits GET credits_route (utas 3765) coins, unopenedPackIds count P R (balance + entitlement count)
/userMassInfo GET massinfo currencies (utas 578) coins, points, record, items, unopenedPackIds, squad P (.squad overlaid R) R economy fields (coins/packs), squad already R
/store/purchasegroup GET store_catalogunopened_packs (utas 3614) unopenedPackIds P + R topology overlay R full-gen (catalog + SessionStore mode + Core entitlements)
/tradePile GET tradepile_route (utas 3911) items, listings, coins P R (reads Core inventory/balance/listings)
/hub,/tradePile/counts,/watchList GET hub_data/auction_counts/watchlist items, listings, coins P R
/club,/club/stats,/user/list,/clubUser GET club readers→items items R (/club Core-backed) / P others R

Writer → Core primitive map (Phase 2)

Python writer Reachable Core primitive (services::economy)
spend (pack buy leg) YES purchase_entitlement debit leg
open_pack YES purchase_entitlement + redeem_entitlement (buy→entitlement→open split)
consume_unopened_pack YES redeem_entitlement (consume-once)
move_items (purchased→club) YES inventory add within redeem_entitlement / move op
add_items (market mint) YES purchase_item (debit + mint)
quick_sell YES sell_item (remove + credit)
record_match (coins) YES grant_reward (credit)
new_item_id YES adapter numeric-id via openfut-identity (Core ids opaque)
list_for_sale / remove_listing YES listing-state (market slice)
grant_coins NO (dead) — drop
grant_unopened_pack NO (test-only) — drop
save_squad YES already Core-authoritative (SquadReplace)

Single-writer rule

Coins live only in Python fut_profile.json today (Core clubs.coins is a separate imported value). Because every coin reader (credits, userMassInfo, tradePile, market bodies) reads that same JSON, the coins cluster must flip readers and writers together — a partial flip desyncs the client's counter (audit "STALENESS RISK"). The coherent first cut is therefore the whole coins bundle: 4 writer routes + credits/userMassInfo/purchasegroup readers, all on Core, seeded by a one-time profile import into Core.

Proxied-route safety (R1 requirement: no unsafe YES)

After cutover, every remaining Python (proxied) route MUST have economy state touched = NONE. Routes with economy state touched != NONE are part of the migration cluster and MUST be Rust before R1. This table is the audit source; the host classify() is the enforcement point (NEVER BOTH).

Cutover progress (2026-08-13)

Landed (Core authority + transport + several handlers; classifier NOT yet flipped):

  • Core economy HTTP API (d32dc6e, bcc4f51): generic /economy/{balance, entitlements,purchase-entitlement,redeem-entitlement,sell-item,grant-reward, purchase-item,purchase-items}, server-side club resolution, atomic. Import now seeds unopenedPackIds→entitlements. Core 178 tests green.
  • Host CoreEconomy client (d240a61): typed reqwest, fail-closed, no Python fallback by contract.
  • Reader handlers: /user/credits (handle_credits), /store/purchasegroup full-gen (handle_purchasegroup, no Python body dependency), userMassInfo economy overlay (overlay_massinfo_economy). Invariant test: all three read one Core state.
  • Writer handler: /match reward (handle_match_end → Core grant_reward, oracle destroy_match_body shape).
  • Adapter policy mappers (181bd94): match reward, pack price.
  • All Core-backed, fail-closed (503, never Python), FakeEconomy-tested.
  • Store/item writers (4d2b8b9, economy_store.rs, unrouted): handle_store_buy (purchase_items debit+mint N → createPackResponse; 461 insufficient; 200 {} cancel/unknown/owned_only), handle_pack_open (owned_only redeem_entitlement consume-once; normal debit+mint), handle_quick_sell{_path,_body} (reverse-resolve wire→Core id via SquadWireResolversell_item). Item wire shaping via fut::item::shape_item; numeric ids via openfut-identity.
  • Pack-content generator (fut/pack_content.rs): pure seeded generate_pack_contents, gold-tier + special_chance PLACEHOLDER policy, fail-closed.
  • Market (market_store.rs/pile_store.rs/market.rs, unrouted): durable sqlx SQLite listing store (WAL+BEGIN IMMEDIATE, states active/reserved/sold/cancelled, CAS reserve/complete/rollback, typed errors), durable pile metadata, handle_market_{list,query,cancel,buy} (synthetic-seller buy-now = reserve→ purchase_item→complete_sale, race-safe two-buyer) + handle_move_items.
  • Adapter +7 / host +43 tests incl. two_reservers_exactly_one_wins, two_buyers_exactly_one_sale_one_debit, state_survives_reopen, move_persists_across_reopen. clippy -D warnings + rustfmt clean.

Not flipped: classify() still routes every economy route to Python. Per the single-writer rule the flip is one coherent barrier once ALL writers+readers are implemented and Core is seeded — a partial flip would desync coins.

Remaining before barrier: (1) sync↔async Server wiring for the new handlers (market/store handlers are async over sqlx runtime-tokio; the host Server is sync — needs a tokio runtime bridge in dispatch) + classify() routes for the new writers/readers; (2) real host↔Core writer E2E in tests/economy_integration.rs (Store BUY/open/quick-sell/market-buy/move against a live Core + disposable DB, extending the existing reader/match harness); (3) Python-oracle differential runner (subprocess, isolated fixture, 13 ops); (4) host-level concurrency matrix across the new handlers; (5) importer disposable-DB apply/restart/idempotency; then the single classifier barrier + no-Python-fallback / writer-unreachable / stale-reader proofs. Retire in-memory ProfileEconomy from any prod path.