diff --git a/openfut-utas-host/ROUTE_AUTHORITY.md b/openfut-utas-host/ROUTE_AUTHORITY.md new file mode 100644 index 0000000..0831d7c --- /dev/null +++ b/openfut-utas-host/ROUTE_AUTHORITY.md @@ -0,0 +1,79 @@ +# 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//match` | POST | `match_route`→`record_match` (fut_store 554) | coins | P | **R** | `grant_reward` | +| `/store/transaction` | PUT | `store_buy`→`open_pack`→`spend` (utas 3702) | coins, purchased, packsOpened, nextItemId | P | **R** | `purchase_entitlement` (+ `redeem_entitlement`) | +| `/purchased` | POST | `purchased_items`→`open_pack`+`consume_unopened_pack`+`move_items` (utas 3716) | coins, purchased, unopenedPackIds, items, nextItemId | P | **R** | `redeem_entitlement` | +| `/ut/game//item/` | DELETE | `quick_sell_url_route`→`quick_sell` (utas 1234) | coins, items, purchased | P | **R** | `sell_item` | +| `/ut/delete/game//item` | POST | `quick_sell_route`→`quick_sell` (utas 1273) | coins, items, purchased | P | **R** | `sell_item` | +| `/ut/game//item` | PUT | `item_route`→`move_items` (utas 1342) | items, purchased | P | **R** | `redeem_entitlement`/move (inventory-only) | +| `/ut/game//trade/` | 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_route`→`list_for_sale` (utas 3865) | listings, nextListingSeq | P | **R** | listing-state (see note) | +| `/ut/delete/game//trade/` | DELETE | `delete_trade_route`→`remove_listing` (utas 3935) | listings | P | **R** | listing-state (see note) | +| `/ut/game//squad` | PUT | `squad_route`→`save_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_catalog`→`unopened_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).