aa2abc2772
Four defects found by driving a real FIFA 17 client. Each was independently
sufficient to break listing, so all four had to go:
1. Every owned card was shaped `untradeable: true` (adapter item.rs), so the
client greyed out "Place/List on Transfer Market" for the whole club. Owned
and pack-pulled cards are TRADEABLE in FIFA 17; the oracle forces this off
for owned copies too (item_def keeps `true`; instances do not).
2. `POST /auctionhouse` required `itemData.resourceId`, which the client's
FutISStart body never sends (the oracle lists by wire id ALONE). Missing it,
the handler fail-closed and returned 200 while persisting NOTHING. It now
resolves server-side: wire id -> Core owned instance -> its card_id (minted on
a synthetic buy) + FIFA resourceId (the auction record). This also enforces
that a listing can only name a card the club actually owns.
3. An auction record's `itemData` was a 4-field STUB, so the Transfer List had a
row the client could not draw -> "1 item listed" but no visible sale. A
listing now persists a full shaped-card SNAPSHOT (new `listings.item_json`,
additive migration) built by the same `shape_item` shaper `/club` and the
squad projection use, so the auction card renders identically to the club
card. The seller's own pile stamps `itemState: listFS`; market search keeps
`forSale` (the oracle distinguishes these).
4. `/tradePile/counts` shared a handler with `/tradePile`. They are DIFFERENT
deserializers: `/counts` is FutGetAuctionCount, five scalar ints
(count/maxAuctionsAllowed/offered/selling/sold) that it reads and skips
everything else. Served the `auctionInfo` body it left every count at 0, so
the Transfer List screen showed no active sale while the hub tile showed one.
New Route::MarketCounts, classified BEFORE the base tradePile matcher (which
also accepts the /counts path).
Also: a listed card no longer appears in the club. `/club` and the hub's
`clubPlayers` now exclude the transfer pile. Pile membership is host-owned state
Core cannot filter on, so when anything is hidden `/club` reuses the existing
local-filter path (the one `rare=SP` already needed) and paginates the
club-visible set -- letting Core paginate would return short pages. With nothing
hidden the fast Core-paginated path is untouched, and only an EXPLICIT non-club
pile hides a card, so no-pile-row items still default to the club.
Fixed 5 pre-existing test fixtures across 4 targets that listed FABRICATED wire
ids -- only "valid" because the old handler skipped the ownership check.
Tests: 14 targets green + clippy clean, incl. new coverage for the 5-int tally
(asserting it must NOT carry auctionInfo), the full-card snapshot + listFS, and
club pile-exclusion with full-width pagination. The differential test against the
live Python oracle passes.
Verified live on prod: listed=true with a 21-field snapshot; counts
{count:1,selling:1,maxAuctionsAllowed:100}; tradePile renders the 94-rated card;
clubPlayers 1966 -> 1961 (exactly the 5 trade-pile items); listed wire absent
from the club page. Operator confirmed the card is visible in the Transfer List.
166 lines
13 KiB
Markdown
166 lines
13 KiB
Markdown
# Production Authority Matrix (post-P1 -> P2-routes promoted 2026-08-17)
|
|
|
|
> **P2-routes promoted to production 2026-08-17.** prod-host swapped P1 `e5be8730` -> post-P1
|
|
> `fda40d12`; catalog `35a0913b` -> `9f6addaa` (resolves all owned assets, dropped_no_asset=0).
|
|
> Every previously-Python non-economy route now RUST in prod (OBSERVED prod log). Rollback hot:
|
|
> restore P1 binary + backup catalog (`economy-2026-08-17-p2/backup/`).
|
|
|
|
Definitive inventory of every production-reachable FIFA17 route/service and its
|
|
current owner. Derived from the live `prod-host` dispatch log (owner= labels,
|
|
real Client A session 2026-08-14), `openfut-utas-host/ROUTE_AUTHORITY.md`, and the
|
|
prod container config (`OPENFUT_SERVERS="blaze roster pow"`).
|
|
|
|
Evidence labels: OBSERVED (live log/db), PROVEN (test), INFERRED, HYPOTHESIS.
|
|
|
|
Legend: owner R = Rust/Core, P = Python oracle (:8199 proxied via PYTHON_FALLBACK).
|
|
|
|
## UTAS HTTP (front door: openfut-utas-host :8099)
|
|
|
|
### ECONOMY — already Rust (Python economy hits = 0, OBSERVED)
|
|
| Method/path (tail) | Prod owner | Writes state | Rust handler | Py proxy |
|
|
|---|---|---|---|---|
|
|
| GET /user/credits | R | no | handle_credits | NO |
|
|
| GET /store/purchasegroup[/all] | R | no | handle_purchasegroup | NO |
|
|
| PUT /store/transaction[/<id>] | R | coins,inv,pile | handle_store_buy | NO |
|
|
| POST /purchased[/items] | R | coins,inv,ent,pile | handle_pack_open | NO |
|
|
| GET /purchased[/items] | R | no | shape_purchased_reveal | NO |
|
|
| DELETE /item/<id> | R | coins,inv | handle_quick_sell_path | NO |
|
|
| POST /ut/delete/../item | R | coins,inv | handle_quick_sell_body | NO |
|
|
| PUT /item | R | inv,pile | handle_move_items | NO |
|
|
| POST /ut/delete/../match | R | coins | handle_match_end | NO |
|
|
| POST /auctionhouse,/transfermarket | R | listings | handle_market_list | NO |
|
|
| GET /tradePile | R | no | handle_market_query | NO |
|
|
| GET /tradePile/counts | R | no | handle_market_counts | NO |
|
|
| /trade/<id> (POST/PUT/GET) | R | coins,inv,listings | handle_market_buy | NO |
|
|
| DELETE /ut/delete/../trade/<id> | R | listings | handle_market_cancel | NO |
|
|
|
|
**Transfer-market semantics (live-verified 2026-08-17).** Three things here are
|
|
load-bearing and were each a live defect:
|
|
1. `/tradePile` and `/tradePile/counts` are **different deserializers** and MUST NOT
|
|
share a handler. `/counts` is FutGetAuctionCount: five scalar ints
|
|
(`count`, `maxAuctionsAllowed`, `offered`, `selling`, `sold`) and nothing else.
|
|
Served the `auctionInfo` body it skips every field, leaving the counts at 0 — the
|
|
hub tile shows a listing while the Transfer List screen shows no active sale.
|
|
2. An auction record's `itemData` MUST be the **full card object** (the same shape
|
|
`/club` emits), not a stub. A listing therefore persists a shaped-card SNAPSHOT
|
|
(`listings.item_json`) at list time. The seller's own pile stamps
|
|
`itemState: listFS`; market search uses `forSale`.
|
|
3. `POST /auctionhouse` resolves the listed card **server-side** from the wire item
|
|
id (`wire → Core instance → card_id + resourceId`); the client's FutISStart body
|
|
carries only the id. This also enforces that you can only list what you own.
|
|
|
|
### NON-ECONOMY — Rust-owned (route migration 2026-08-17, OBSERVED prod log)
|
|
| Route | Owner | Notes |
|
|
|---|---|---|
|
|
| POST /ut/auth (+ /ut/delete/auth) | R | Rust mints the SID (OPENFUT-SID-{:016X}); opens Rust session; adopts persona from body. No Python. |
|
|
| POST /openfut/account/sync | R | full Rust envelope; coins/unopenedPacks from Core; clubName OpenFUT / clubAbbr OFC constants |
|
|
| GET /userMassInfo | R | FULL Rust envelope (userInfo+squad+settings+pileSizeClientData); no Python. coins from Core, squad == /squad/active |
|
|
| GET/PUT /clientdata/<key> | R | host ClientDataStore (JSON-persisted); PUT acks {}, GET returns blob or {} |
|
|
| capability (/openfut/fifa17/capability) | R | -> Bound (CleanV1) |
|
|
| GET /club, /club/* readers | R | Core-backed collection (dropped_no_asset=0). Cards in the **transfer pile are excluded** — a listed card has left the club. Pagination then runs over the club-visible set (Core cannot filter on host-owned pile state, so letting it paginate would yield short pages); with nothing hidden the fast Core-paginated path is unchanged. Only an EXPLICIT non-club pile hides a card, so no-pile-row items default to the club. |
|
|
| GET /squad/0, /squad/active, /squad/list; PUT /squad/<n> | R | Core squad projection + tx; GET /squad/0 == active squad (verified structurally identical) |
|
|
| GET /user/accountinfo | R | {} |
|
|
| GET /user | R | `{"userInfo": …}` — same userInfo builder as userMassInfo (shared); squad rating is Core-authoritative (DIFFERENT-BY-DESIGN vs Python's stale value) |
|
|
| GET /settings | R | {"configs":[]} |
|
|
| GET /leaderboards/options | R | {} |
|
|
| PUT /match/reset | R | {} |
|
|
| GET /phishing/trusteddevice | R | security-question stateless ack |
|
|
| GET /hub | R | Core-derived counts; `clubPlayers` excludes transfer-pile cards (1966 → 1961 with 5 listed/moved), `auctionCount`/`tradePile` from the durable market store |
|
|
| GET /club/stats/{year,consumables,staff,country,league,team} | R | Core aggregation; context buckets keyed nation/league/team (owned=1982); staff={} |
|
|
| GET /store, /match/keepalive, /captcha, /tfa, /livemessage, /activeMessage | R | unconditional constant acks (byte-identical to the oracle; StaticAck route) |
|
|
| GET /watchList (+ PUT/POST/DELETE) | R | empty watch list + authoritative Core credits; add/remove is a no-op ack (oracle persists none) |
|
|
| GET /season, /tournament, /champion, /clubUser, /user/list | R | FUT modes + club-identity off → `{}` (FeatureOffEmpty; byte-identical to the flag-off oracle). Migrated + deployed 2026-08-17 |
|
|
| POST /ut/.../match/end (DestroyMatch) | R | economy reward (coins credited via Core grant_reward) |
|
|
| GET /item/resource, /defid | R | `{itemData:[item_def…]}` — asset=rid&0xffffff; hardcoded Ronaldo (20801) + placeholder ("Player",75,CM,attrs 70), mirroring the oracle's `item_def`. Client renders from its LOCAL DB, so the placeholder is exact parity. Migrated + deployed 2026-08-17 |
|
|
| GET /marketdata, /marketdata/pricelimits | R | suggested pricing, constant band 150..15000. `/pricelimits` = bare ARRAY (one per defId); plain `/marketdata` = OBJECT — container type is load-bearing (object-where-array froze a live client). Migrated + deployed 2026-08-17 |
|
|
|
|
### NON-ECONOMY — still Python (PYTHON_FALLBACK)
|
|
| Method/path | Owner | Reason |
|
|
|---|---|---|
|
|
| POST /user/club (rename) | P | mutating club rename; Core has `clubs` but rename needs a Core write (deferred). Reads `clubUser`/`user/list` are now Rust. |
|
|
| GET /squad/<n> (n≠0, non-active) | P | no multi-squad Core model (Rust owns squad/0, squad/active, /squad/list, PUT) |
|
|
| /squad/mode/draft/* | P | FUT Draft mode |
|
|
| GET /leaderboards, /sbs/* | P | mode-gated (FUT_MODES/_SBC off → `{}`/content; `/sbs/sets` ships content); real behavior needs the mode logic ported. `season`/`tournament`/`champion` are now Rust. |
|
|
| POST /ut/.../match (CREATE), /match/ready (READY), /match (PLAY) | P | match handshake legs; no match ever played in-game (see docs/MATCH_LIFECYCLE.md) |
|
|
|
|
## AUXILIARY SERVICES (prod container OPENFUT_SERVERS="blaze roster pow")
|
|
All aux services are **Python in production today** (container `entrypoint.sh` runs
|
|
`blaze_responder_v3b.py`/`roster_server.py`/`pow_server.py`). Rust equivalents exist
|
|
outside Docker; deploying them is operator-gated (production deployment forbidden here).
|
|
|
|
| Service | Rust crate | Completeness | Prod owner | Reachable | Blocker to candidate |
|
|
|---|---|---|---|---|---|
|
|
| Blaze (:42130) | openfut-blaze-host + openfut-protocol-blaze + adapter::blaze | COMPLETE, gate-proven to real FUT (Gate 10: 3 logins, 10 pack opens, 448/448 py suite) | Python | yes | ERRC error-reply placement unresolved; wire into candidate bring-up |
|
|
| Redirector (:42127 TLS) | openfut-redirector-host + openfut-tls (OpenSSL vendored) | COMPLETE, 1 live handshake 2026-08-11 (TLSv1.2/AES256-GCM-SHA384) | Python | yes | never full-path gated; cert consistency |
|
|
| Roster (:8081) | openfut-roster-host | COMPLETE, oracle-parity + lifecycle tests, unit-only | Python | yes | never live-gated |
|
|
| POW (:8094 + :8080) | NONE (only pow_server.py) | no Rust host; 58 templates, bodies placeholder | Python | yes | LARGEST: no crate + bodies un-reversed |
|
|
| Nucleus (:42131) | none (advertised string only) | n/a | Python stub (advertised, unused) | NO (0 live hits) | DEAD in current flow — keep advertising URL, no migration |
|
|
|
|
RETIRED/out-of-scope: fifa-blaze (FIFA23 capture stub), openfut-bridge (FIFA23). Not in FIFA17 flow.
|
|
|
|
## NON-ECONOMY UTAS TARGET OWNER (confirmed via scouts)
|
|
Core already exposes GET/PUT /settings, GET /club|/collection|/statistics|/profile, /squad/*.
|
|
New Rust arm = Route variant + classify() arm (lib.rs:118-140) + owner-labelled handler.
|
|
|
|
| Route | Port complexity | Target | Notes |
|
|
|---|---|---|---|
|
|
| user/accountinfo | trivial-static ({}) | R | host constant |
|
|
| settings | trivial-static ({"configs":[]}) | R | host constant / Core /settings |
|
|
| leaderboards/options | trivial-static | R | host constant |
|
|
| match/reset | trivial-static ({}) | R | host constant ack |
|
|
| phishing/trusteddevice | small (validate hex + constant) | R | security-question: stateless ack, always verified/trusted |
|
|
| clientdata/userHubData | small stateful | R | Core PUT /settings upsert keyed userHubData |
|
|
| hub | medium (derived counts) | R | Core collection + market counts |
|
|
| club/stats/year | medium (rating-tier aggregation) | R | Core /collection + /statistics |
|
|
| club/stats/consumables | medium | R | BLOCKED on 17-consumable import |
|
|
| club/stats/staff | trivial ({}) or derived | R | BLOCKED on 3-staff import |
|
|
| account/sync | small-medium (persona select + save) | R | hardest: no direct Core route; host session/persona logic |
|
|
| ut/auth envelope | small (SID mint) | R (currently OBSERVE) | Python still mints envelope; boundary decision |
|
|
|
|
## MIGRATION PRIORITY ORDER (Phase 12)
|
|
1. Content gap consumable+staff emit (unblocks club/stats/{consumables,staff}) — import crate.
|
|
2. Trivial-static host routes: accountinfo, settings, leaderboards/options, match/reset, phishing/trusteddevice.
|
|
3. clientdata/userHubData (Core settings upsert).
|
|
4. hub + club/stats/year (Core-derived aggregation).
|
|
5. userMassInfo full ownership (envelope scaffold; econ+squad already Rust).
|
|
6. account/sync + ut/auth envelope (account/session boundary).
|
|
7. Aux candidate wiring (blaze/roster/redirector already built) + POW (blocked) + Nucleus (dead).
|
|
|
|
## ECONOMY EXPECTATION
|
|
Python economy hits = 0 (OBSERVED live + PROVEN by NEVER-BOTH/no-fallback tests).
|
|
Any nonzero Python economy hit = P1 regression, priority zero.
|
|
|
|
## STATUS
|
|
Baseline + economy + Rust-owned non-economy rows: OBSERVED/PROVEN.
|
|
Aux + non-economy target owners: confirmed via HostSourceMap/PythonContractMap/AuxServiceMap/ContentGapMap scouts.
|
|
|
|
## CANDIDATE MIGRATION STATUS (post-P1 progress, off-production)
|
|
DONE (Rust-owned in candidate source; committed on top of 5020137; workspace tests green):
|
|
- Content gap: consumable+staff emit (20 instances / 18 defs; verified real-profile re-import 1962+20).
|
|
- Non-economy routes migrated to Rust: user/accountinfo, settings, leaderboards/options,
|
|
match/reset, phishing/{trusteddevice,question,validate}, club/stats/staff, hub,
|
|
club/stats/{year,consumables}.
|
|
(hub clubPlayers = owned player count from Core; may be < Python profile count by the deferred
|
|
Legend instances = DIFFERENT-BY-DESIGN.)
|
|
- Reachability reporter (scripts/openfut-reachability.py) gates Python-hit invariants.
|
|
|
|
RESIDUAL PYTHON (still proxied; each has a concrete blocker, not ordinary difficulty):
|
|
- club/stats/{country,league,team} — RESIDUAL (nation/league/team context sub-screens). The global
|
|
MY-CLUB stat set (club/stats/{year,consumables,staff}) is now Rust (adapter club_stats.rs faithful
|
|
port of fut_club_stats.py VOCAB + counts, Core-accurate over real imported content, staging-verified
|
|
RUST route=club-stats owned=1982 players=1962). The per-nation-bucket context sub-screens still proxy
|
|
Python (low value, unrecognized atoms inert); migrate with a live context capture if ever needed.
|
|
- account/sync — launcher-facing (POST /openfut/account/sync, pre-auth); envelope has fields not in
|
|
Core (clubAbbr, established, profilePath, accountFunds cap); changing it risks the launcher. Needs
|
|
a Core account endpoint or host account logic + launcher-contract verification.
|
|
- userMassInfo — SAFE hybrid today (Rust overlays economy+squad on Python envelope). Full ownership
|
|
needs a real full-envelope capture first (missing one scaffold field breaks a hot route); deferred.
|
|
- ut/auth — ORDERING-BLOCKED: still-proxied Python routes rely on Python's session table, so auth
|
|
must stay proxied (Python mints SID, Rust OBSERVEs) until every session-needing route is Rust. Migrate LAST.
|
|
- clientdata/userHubData — BLOCKED: Core /settings supports only 2 fixed keys (difficulty,
|
|
preferred_formation); storing an arbitrary blob needs a frozen-Core change or a new host store.
|
|
Low-value UI-pref blob; kept Python per Phase 23 (no Python-by-ideology).
|
|
|
|
AUX (Rust built, deploy = operator-gated container cutover, NOT this task): blaze/roster/redirector.
|
|
POW = blocked (no crate, bodies un-reversed). Nucleus = dead (0 live hits).
|