Commit Graph

286 Commits

Author SHA1 Message Date
funman300 2ecd830d75 docs(evidence): commit fresh sanitised UTAS wire captures (2026-08-15 live A/B)
Rebuilds the primary-capture corpus lost to .gitignore (Known Issues #200): 10
real-client requests + 12 responses captured during the post-P1 staging A/B on a
real FIFA 17 client, sanitised (SID/authCode/deviceId/MAC/tokens redacted; raw pcap
withheld). Documents the account/sync, empty-My-Packs 65534 sentinel, and userMassInfo
contracts, incl. the finding that account/sync is coupled to Python active-profile
selection (so it can't migrate standalone from the userMassInfo hybrid).
2026-08-17 04:09:56 +00:00
funman300 3a51b0ebd4 docs: correct wrong Fire2 header traps in heat2.py + fifa-blaze frame.rs
Both files documented a wrong Fire2 header layout as authoritative, the reader
trap called out in Known Issues:
- heat2.py's module docstring labelled its >IHHHHB3s header 'VALIDATED'. The
  round-trip only validates the payload length + TDF body; decode->encode with the
  same mislabelled header trivially reproduces the capture, so it never tested the
  [10:16] field boundaries. Marked superseded; cite the proven layout; warn at
  build_fire2_frame. Code unchanged (dead tooling).
- fifa-blaze frame.rs: see submodule commit f4f3396.

Bumps fifa-blaze submodule eccd46f -> f4f3396 (FIFA23 stub; not in the prod
container; no prod impact).
2026-08-16 21:29:52 +00:00
funman300 ad406f21bd fix(tls): share bare-probe classification across all FIFA-facing TLS hosts
A reachability probe (TcpStream::connect then drop; the launcher preflight makes
them) reaches a TLS acceptor as 'unexpected EOF' — byte-identical to the
certificate mismatch that cost three live gates. The redirector classified the
opening before the acceptor to keep a benign probe from forging a TLS fault, but
the roster host (the second FIFA-facing TLS host) did not, so the documented
hazard 'remains in any other TLS host that has not adopted it' was live there.

Lift the pure policy (PeerOpening + classify_opening) plus a peer_opening(&TcpStream)
peek helper into the shared openfut-tls crate (game-independent; +unit tests).
The redirector now re-exports them (public API + its probe_classification test
unchanged; behaviour identical). The roster host adopts them: a ProbeCount, a
probes() handle, and a pre-acceptor peek that logs PROBE and returns instead of
failing the handshake. New roster probe_classification integration test (3 cases:
bare probe classified, real client after a probe still served 200, speaks-then-
fails still reported as a fault). Full workspace tests green; clippy -D clean.
2026-08-16 20:30:50 +00:00
funman300 12fb9fc38b chore: update workspace Cargo.lock after excluding openfut-hook
openfut-hook (now its own workspace root) and its windows-sys deps are no longer
part of this workspace's lockfile.
2026-08-15 19:32:18 +00:00
funman300 7b580a0070 chore: bump openfut-hook clippy -D warnings cleanup (0d3f33c -> d1a71bd) 2026-08-15 19:31:25 +00:00
funman300 22443a3810 build(hook): exclude openfut-hook from workspace so its release profile applies
openfut-hook (Windows version.dll injected into the FIFA client) declared a
[profile.release] with panic=abort/strip/opt-level=s that Cargo silently ignored
because it was a non-root workspace member (per-package `panic` overrides are
forbidden). Move it out of `members` into `exclude`; the submodule now carries a
matching empty [workspace] table so it builds as its own root. Fixes cross-FFI
panic-unwind UB in the injected DLL, shrinks it 1200126 -> 861696 B, and lands the
artifact in openfut-hook/target/ (matching launcher config.rs hook_dll_path).

Bumps openfut-launcher submodule ca7ce26 -> 0d3f33c.
2026-08-15 19:25:19 +00:00
funman300 0fce1e521c docs: mark club/stats/{year,consumables} Rust-owned in authority matrix
Global MY-CLUB stat set now Rust (staging-verified RUST route=club-stats
owned=1982 players=1962); only club/stats/{country,league,team} nation-bucket
context sub-screens remain proxied (low value, inert atoms).
2026-08-15 18:04:23 +00:00
funman300 71fcf5e251 feat(fifa17): own club/stats/{year,consumables} in Rust (Core-accurate)
Migrate the MY CLUB stat set from the Python proxy to a Rust handler computing
Core-accurate counts: player tiers + rare from the collection, staff/consumable
families from catalog kind+subtype, per-nation buckets via the reverse entity
resolver. Faithful port of fut_club_stats.py (VOCAB + global_counts +
context_rows). Unlike the oracle (stale profile + synthetic consumable shelf),
this reflects the real imported content (incl. the content-gap consumables/staff).
Fail-closed 503 on Core error. club/stats/country|league|team sub-screens remain
Python (documented). Adds adapter club_stats module (5 tests), host handler +
classify arm + resolver subtype_of/rareflag_of, ownership + integration tests;
reachability tool splits club/stats global(migrated) vs context(residual).
2026-08-15 17:56:37 +00:00
funman300 979e71fbea docs: record precise blockers for remaining Python non-economy routes 2026-08-15 17:31:15 +00:00
funman300 45e0b0bd95 docs: mark hub + club/stats/staff migrated in authority matrix 2026-08-15 17:18:55 +00:00
funman300 70eb3fc13f feat(fifa17): own FUT hub tile counts in Rust
Migrate GET /hub from the Python proxy to a Rust handler deriving counts from
authoritative state: clubPlayers = owned PLAYER cards in Core (consumables/staff
excluded via catalog kind; may be lower than Python's profile count by the
deferred Legend instances = DIFFERENT-BY-DESIGN), auction/tradePile counts from
the durable market store via the async bridge. Fail-closed 503 on Core error;
market read failure degrades cosmetic counts to 0. Adds classify arm, handler,
ownership + integration tests; reachability tool marks hub migrated.
2026-08-15 17:18:06 +00:00
funman300 b30aa352f6 docs: record post-P1 candidate migration status + clientdata Core blocker 2026-08-15 17:13:47 +00:00
funman300 67cc33cfee feat(fifa17): own club/stats/staff (empty stat set) in Rust
Migrate GET club/stats/staff from the Python proxy to a Rust static handler.
The production oracle returns {} for the staff-bonus stat set (deliberately
empty); the Rust host now owns it (owner=RUST route=club-stats-staff). Updates
classify(), the pre-existing near-miss test (now club/stats/year), the ownership
matrix test, and the no-fallback integration test. club/stats/{year,consumables}
remain Python (aggregation) pending the Core-derived club-stats migration.
2026-08-15 17:13:16 +00:00
funman300 6eec3b9ec7 test(fifa17): add UTAS route-reachability reporter (Python-hit gate)
Parses the host owner= dispatch log into per-owner + per-domain counts and gates
on the post-P1 invariants: economy Python hits == 0 (P1 regression), migrated
non-economy routes (accountinfo/settings/leaderboards/match-reset/phishing) ==
0, and residual Python domains == documented set. Read-only; staging-preflight
and Phase 40 live-ownership use.
2026-08-15 17:01:11 +00:00
funman300 57773b98ec docs: Python retirement readiness classification (post-P1) 2026-08-14 05:36:32 +00:00
funman300 fe9b899a0e docs(fifa17): record .105 Legends unrecoverable verdict (dcplayernames empty) 2026-08-14 05:26:02 +00:00
funman300 abe9e663c1 feat(fifa17): import consumable + staff content as first-class Core content
Close 20 of the 33-record content gap (17 consumables + 3 staff; 13 Legends are
unrecoverable from PC data). Verdict A (no Core change): consumables/staff become
ordinary Core CardDefinitions (neutral player fields + honest family/role names)
and owned instances via the SAME generic import path; a catalog kind lets the
adapter exclude them from the player-only /club projection.

- adapter fut::content_taxonomy: evidence-based cardsubtypeid->family/label
  (Ghidra-derived ranges) + staff role map; unknown subtype => defer, never fabricate.
- adapter catalog: Fifa17CardIdentity/RawCard gain optional kind+subtype
  (backward-compat: legacy catalogs load as player); kind_of/subtype_of lookups.
- adapter item/club_response: shape_club_response excludes non-player kinds
  (ShapeStats.excluded_non_player); ItemIdentityResolver::kind_of default=Player.
- host Fifa17IdentityResolver overrides kind_of to delegate to the catalog so
  /club excludes consumables/staff in production.
- import: Item gains cardsubtypeid/cardassetid/amount/contract; plan_non_player_definitions
  (resourceId-grouped, subtype-consistency gated); emit_content writes non-player
  defs + catalog kind + manifest; apply mints owned instances via owned_item_id.

Real profile 33068179: 1962 players + 20 non-player = 1982 owned; 18 non-player
defs (16 consumable + 2 staff, dup resourceIds shared); 0 deferred non-player; 0 blockers.
2026-08-14 05:26:02 +00:00
funman300 f5a33eb58c test(fifa17): prove non-economy routes are Rust-owned with no Python fallback 2026-08-14 05:08:37 +00:00
funman300 a85090c3c6 feat(fifa17): own non-economy static + security-question routes in Rust
Migrate 5 non-economy UTAS route families from the Python oracle proxy to
Rust host ownership: user/accountinfo, settings, leaderboards/options,
match/reset, and phishing/{trusteddevice,question,validate}.

- adapter fut::non_economy: pure IO-free shapers matching the observed prod
  oracle bodies + a verbatim port of security_question_route (stateless ack;
  answer never stored/compared; trusted-device is an invariant constant).
- host: Route variants + classify() arms + owner=RUST dispatch; the
  security-question X-UT-SID gate reuses SessionStore::session_known.
- tests: 9 adapter unit tests (contract) + host non_economy_route_ownership
  (classify + classify_economy negatives).
2026-08-14 04:57:28 +00:00
funman300 97d48d8371 docs: record post-P1 production authority matrix + FIFA17 content completeness 2026-08-14 04:57:28 +00:00
OpenFUT Agent 5020137050 docs(fifa17): record retail economy route grammar (purchased/items, tradePile) 2026-08-14 00:50:03 +00:00
OpenFUT Agent cf05ab2a9e test(fifa17): replay retail purchased/items BUY + route matrix via dispatch
- pure_economy_routes (NEVER-BOTH + no-fallback) gains POST/GET purchased/items,
  lowercase tradepile, tradePile/counts -> all asserted Rust-owned, proxy 0.
- retail_purchased_items_buy_debits_core_through_dispatch: the exact round-2
  live failure -> POST /purchased/items now debits Core, reveal shows minted
  items, repeat reveal idempotent, Python proxy count 0.
2026-08-14 00:50:03 +00:00
OpenFUT Agent a6416a3f1d fix(fifa17): classify full retail economy route shapes
Round-2 live staging (candidate 47ced22) showed the CONFIRMED retail Store BUY
uses POST /ut/game/fifa17/purchased/items (reveal GET .../purchased/items),
which the exact-tail 'purchased' match missed -> Python (Core coins unchanged).
Comprehensive audited fix in classify_economy:
- is_purchased_tail: 'purchased' AND 'purchased/items' (POST->PackOpen,
  GET->PackReveal); bounded (rejects purchasedfoo, purchased/items/extra).
- is_tradepile_tail: 'tradePile' family CASE-INSENSITIVE incl 'tradePile/counts'
  (hub tile polls lowercase; oracle routes via re.I); allocation-free.
- (kept) v1/v2 prefix normalization + store/transaction[/<digits>].
Adds retail_route_matrix unit test = the machine-auditable route contract gate
(all economy shapes + negative near-misses). Host lib 75.
2026-08-14 00:50:03 +00:00
OpenFUT Agent 47ced228de docs(route-authority): record v1/v2 economy URL prefix contract
Accepted prefixes /ut/game/<sku>/ and /ut/v2/game/<sku>/ for every economy
route; StoreBuy accepts store/transaction and store/transaction/<txn-id>.
2026-08-13 23:53:44 +00:00
OpenFUT Agent b8beeba98d test(fifa17): cover retail v2 Store route shapes
Through real handle_with_ip dispatch against live Core:
- pure_economy_routes gains the retail v2 Store family (transaction/0,
  purchasegroup, purchased GET/POST) so NEVER-BOTH + no-fallback assert them
  Rust-owned (Python proxy count 0) and fail-closed 503 on dead Core.
- Part-7 repro: PUT /ut/v2/game/fifa17/store/transaction/0 returns a Rust
  createPackResponse + debits Core (NOT the Python TRANSACTIONCANCEL no-op).
- retail_v2_store_flow_matches_v1_through_dispatch: v1 and v2 BUY of the same
  pack debit + mint identically; v2 purchasegroup + reveal Rust-owned.
2026-08-13 23:53:44 +00:00
OpenFUT Agent df6994c957 fix(fifa17): classify retail v2 economy routes
Live staging (S2) showed the retail FIFA17 client issues the Store family
under /ut/v2/game/<sku>/... (PUT /ut/v2/game/fifa17/store/transaction/0),
which escaped Rust economy authority to Python. Fix classify_economy:
- ut_tail() normalizes both /ut/game/<sku>/ and /ut/v2/game/<sku>/ to the
  same tail (generic sku, never hard-coded fifa17); delete family likewise
  accepts /ut/v2/delete/game/.
- StoreBuy matches store/transaction and store/transaction/<digits> via a
  bounded is_store_transaction_tail (never store/transactions, ...foo, or
  .../<id>/extra), mirroring the Python bare /store/transaction regex.
Adds table-driven ut_tail + is_store_transaction_tail + classify_economy v2
unit tests (lib 74).
2026-08-13 23:53:44 +00:00
OpenFUT Agent d74e86c065 docs(fifa17): record Rust economy authority proof (E1 cutover ready)
Final per-route authority table: every economy route owner=Rust, Python
proxy=NO (userMassInfo the one hybrid: Python envelope + Rust economy/squad
overlay). Barrier 93a46d4; from_config 43917a0. Proofs: differential 15 PARITY
+ 1 DIFFERENT-BY-DESIGN, concurrency 8x50, failure 10 (complete-sale SAFE, no
E3), importer 5-step, from_config E2E, NEVER-BOTH / no-fallback / stale-reader.
Python source byte-unchanged; oracle suite 32/32.
2026-08-13 22:44:12 +00:00
OpenFUT Agent 76512f6048 test(fifa17): prove post-barrier economy authority (never-both / no-fallback / stale-reader)
barrier_never_both_no_fallback_and_stale_reader drives the REAL post-barrier
handle_with_ip against a live in-process Core + a mock Python upstream that
counts every request and answers with a coins=111 marker:
- STALE READER: credits + userMassInfo show the Core balance, never 111
  (userMassInfo proxies the Python envelope but the Rust economy overlay wins).
- NEVER BOTH (Core up): every pure economy route returns a Rust body (no
  __python__ marker) and the Python proxy call-count stays 0.
- NO FALLBACK: a server pointed at a dead Core port (built without probing Core:
  empty catalog + empty pool) fails closed (credits/match -> 503) and STILL never
  proxies to Python (call-count unchanged).

Also parametrizes build_econ_server's pass URL so the mock upstream can be
injected. host 71 lib + 4 integration + differential + concurrency + failure +
24 host_test all green; clippy -D warnings + fmt clean.
2026-08-13 22:39:45 +00:00
OpenFUT Agent 93a46d4de7 feat(fifa17): cut over FUT economy authority to Rust
The economy authority barrier. `handle_with_ip` now dispatches every
economy-touching route to Rust/Core via `try_handle_economy` BEFORE consulting
`classify()`, so a migrated route can never also reach the Python passthrough
(NEVER BOTH). With economy services wired (production `from_config`) an economy
route ALWAYS returns Some — fail-closed 503 on any Core error — so there is no
Python economy fallback. `userMassInfo` stays a hybrid by design: Python
supplies the non-economy envelope; Rust overlays BOTH the squad and the economy
fields (coins + unopened-pack count from Core), so no stale Python economy value
is visible.

Routing only — no handler/test changes buried here. Routes now Rust-owned:
/user/credits, /store/purchasegroup, /store/transaction, POST+GET /purchased,
PUT /item, item DELETE forms, /match (ut/delete), /auctionhouse, /tradePile,
/trade, ut/delete trade; plus the userMassInfo economy overlay.
2026-08-13 22:39:36 +00:00
OpenFUT Agent 3ba24a0faf test(import): verify durable FIFA17 economy migration
openfut-import-fifa17/tests/durable_import.rs drives the REAL import pipeline
(analyze -> Report dry-run; emit_content; plan_apply -> GenericImportRequest +
deterministic identity mappings + watermark; the staging/preflight/seed/
post-validate gates over a real JsonIdentityStore; openfut_core::services::
import::apply_profile_import in one Core SQLite tx) against a disposable
temp-file Core DB, from a small sanitized in-test fixture (750000 coins, 3
resolvable base players, unopenedPackIds [70,70,101], one squad).

Five ordered steps on one durable target, all green:
  A dry-run: report exposes persona/coins/inventory/unopened/fingerprint; ZERO
    DB mutation (all Core tables COUNT=0, identity store empty).
  B apply: coins=750000 exact, owned=3, packs=3 (opened=0), squad_players=2,
    deterministic owned ids, import_fingerprint recorded, identities reverse-
    resolve both ways, watermark=100000600.
  C restart: close+reopen the SAME sqlite file -> identical state.
  D re-apply same source -> AlreadyImported (fingerprint), no doubling.
  E conflict (coins 750000->750001 flips the fingerprint for the same game)
    -> apply fails closed ('different source'); DB unchanged.

Fingerprint = FNV-1a-64 hex of the source snapshot, carried into
ProfileImportRequest.source_fingerprint = Core profiles.import_fingerprint, the
per-game rerun-identity key. dev-deps added to openfut-import-fifa17
(openfut-core path, tokio, sqlx). No production/live data.
2026-08-13 22:31:01 +00:00
OpenFUT Agent 6926bb9528 test(fifa17): add Python-oracle economy differential coverage
economy_differential.rs boots the REAL Python oracle (fifa17-recon/tools/
utas_server.py) as an isolated subprocess (env FUT_PROFILE/FUT_ACCOUNT_PATH/
FUT_PORT into a temp dir + loopback port; no production container/port/save;
killed on Drop) AND the real Rust stack (seeded in-process Core + a real Server
with EconomyServices), seeds a semantically-aligned fixture on both, and drives
16 ops through the REAL surfaces (oracle over HTTP; Rust via
Server::try_handle_economy on the off-runtime thread).

Result: 15 PARITY, 1 DIFFERENT-BY-DESIGN.
- PARITY: credits, userMassInfo economy, purchasegroup (pack70/sentinel/clean-v1
  incl. the real SessionStore capability handshake), Store BUY, POST /purchased
  open, GET /purchased reveal (VERIFIED: durable single-profile purchased pile
  on BOTH — the hypothesised per-SID cache does NOT exist, so PARITY not
  DIFFERENT-BY-DESIGN), quick-sell (both forms), move, match WIN (+400 byte
  shape), market list/query/cancel.
- DIFFERENT-BY-DESIGN: market second-buy. First buy debits buyNowPrice + closes
  on both. Rust's MarketStore is a crash-consistent single-debit ledger (second
  buy of a sold listing = no-op, pinned by assertion); the oracle's buyable
  market is a stateless PACK_POOL sample that re-debits on repeat. Compat impact
  NONE (buy-now is one-shot); Rust is a strict correctness improvement.

No Python source changes; no classifier changes. Deterministic (3 runs).
2026-08-13 22:30:47 +00:00
OpenFUT Agent b1643309f6 test(fifa17): prove host economy concurrency and failure rollback
Two real host-dispatch test files (no fakes) driving Server::try_handle_economy
against a live in-process Core over the real blocking client + durable
MarketStore/PileStore + JsonIdentityStore, each racer its own OS thread
(off-runtime pattern).

economy_concurrency.rs — 8 races x 50 iterations:
  A two BUYs (coins for one) -> exactly one 200 + one 461, final 0, one debit.
  B duplicate owned-pack open -> one redemption, +11 once, entitlement once.
  C duplicate quick-sell -> one sell + one credit + one removal.
  D two market buyers -> one win, one debit, one mint, sold once.
  E reward+BUY -> no lost update (Core relative UPDATE under BEGIN IMMEDIATE).
  F move+quick-sell / G list+quick-sell -> one coherent transition.
  H 1000 concurrent mints -> unique + reversible wire ids, monotonic watermark.

economy_failure.rs — 10 fault-injection sub-cases, all fail-closed:
  BUY/open-redeem/generator/pile/identity, quick-sell, move, market
  reserve/purchase/complete. CRITICAL complete-sale-after-commit = SAFE: the
  listing is left `reserved` (not active), so the active->reserved reserve CAS
  can never win again -> not buyable, exactly one debit + one mint. No E3.

Fault injection uses test-file CoreEconomy/ExternalIdentityStore doubles plus a
NARROW, inert-by-default `StoreFault` seam in market_store.rs + pile_store.rs
(the concrete stores have no trait boundary; 3 `tripped()` checks + a field,
zero behaviour unless a test arms it). `parking_lot` promoted to a normal dep
(the seam's Mutex is used at lib scope). Classifier/ROUTE_AUTHORITY/Python
untouched. host lib 71/71; both new tests pass.
2026-08-13 22:30:35 +00:00
OpenFUT Agent 43917a0051 feat(fifa17): attach economy services in Server::from_config
Wire the PRODUCTION constructor so the economy authority is not test-only.
Server::from_config now builds one process-lifetime AsyncBridge, opens the
durable MarketStore + PileStore (paths from config), shares one HttpCoreClient
as both CoreAccess and CoreEconomy, builds the content pool from Core, and
attaches EconomyServices via with_economy. Stores/bridge are host-lifetime, never
per request.

- config.rs: required OPENFUT_MARKET_DB / OPENFUT_PILE_DB (durable file paths;
  must survive host restart — no temp defaults).
- Fail-closed startup: a bridge/store that cannot initialize returns Err from
  from_config (host refuses to start) — NEVER a silent omission or a Python
  economy fallback.

Test: from_config_constructs_and_serves_economy — builds the Server via the REAL
from_config (disposable config: temp market/pile/identity + a catalog file
derived from seeded content + the real tables dir) against a live Core, drives
credits / purchasegroup / Store BUY / market list-query-buy through it, then
rebuilds from the SAME config after a Core restart and asserts the balance
persisted. host 71 lib + 3 integration + 24 host_test green; clippy/fmt clean.
2026-08-13 21:59:29 +00:00
OpenFUT Agent 1fac71e3ef docs(route-authority): market resourceId mapping + reveal contract landed
Records fe72f0d (resourceId->Core card_id reverse mapping; listings carry both
identities; full Core+store restart E2E) and 747cc23 (GET /purchased reveal =
durable purchased pile, idempotent). Both pre-barrier correctness gaps closed.
Remaining: Python differential, host concurrency matrix, failure injection,
importer, from_config attachment, then the classifier barrier + reachability.
2026-08-13 21:51:52 +00:00
OpenFUT Agent 747cc234c1 fix(fifa17): preserve owned-pack reveal state for GET /purchased
Closes the reveal contract gap: POST /purchased opens a pack and returns
metadata; the client then polls GET /purchased for the opened items. Store BUY
returns items inline, but owned reward-pack (e.g. pack 70) opens had no reveal
read path, so a real FIFA session would show nothing after opening.

Faithful to the Python oracle (fut_store.last_pack / purchased pile): the reveal
is the set of owned items currently in the FIFA "purchased" pile — durable,
idempotent on repeat GET, cleared per-item when a card is moved to the club, and
appended-to by each open. Not a replay cache; presentation state derived from
the durable pile store + Core inventory.

- pile_store.rs: `list_by_pile(pile) -> Vec<core_item_id>` (reveal membership).
- economy_store.rs: `PurchasedPileSink` trait + optional `StoreDeps.purchased`;
  handle_store_buy/handle_pack_open record each minted item into the "purchased"
  pile. `shape_purchased_reveal` (pure): filter Core inventory to the purchased
  pile, shape with the SAME `shape_club_response` /club uses. Grants nothing,
  consumes no entitlement, allocates no id, moves no coins.
- lib.rs: EconomyRoute::PackReveal + classify_economy (GET purchased);
  BridgedPurchasedSink (records via the runtime bridge from the sync dispatch
  thread); dispatch reads the pile async + Core inventory sync + pure-shapes.

Scoping: single fifa17 profile/club (like the Python oracle), so all sessions
share one purchased pile — DIFFERENT-BY-DESIGN vs a per-SID cache, matching the
oracle's single-profile model.

Tests: pile_store::list_by_pile_filters_and_reflects_moves; and the dispatch E2E
now opens pack 70 (entitlement seeded via the Core economy API) and asserts GET
/purchased reveals the opened items and is idempotent on repeat. host 71 lib +
2 integration + 24 host_test green; clippy -D warnings + fmt clean.
2026-08-13 21:51:12 +00:00
OpenFUT Agent fe72f0def2 fix(fifa17): map market resource ids to authoritative Core card ids
Closes the market correctness gap: handle_market_list recorded listing.card_id
from the raw FIFA wire resourceId, so a synthetic buy minted a card_id Core
could not resolve — it survived the immediate response but Core's content
preflight rejected it on reboot.

- catalog.rs: keep the by_resource reverse index (was built then discarded) and
  expose `card_id_for_resource(resource_id) -> Option<&str>` — exact reverse of
  the card_id->asset catalog, no heuristics, unknown => None.
- lib.rs: `impl MarketCardResolver for Fifa17IdentityResolver` delegates to the
  same catalog /club shaping uses; Core never sees a FIFA resource id.
- market_store.rs: listings now carry BOTH `card_id` (authoritative Core content,
  what a buy MINTS) and `wire_resource_id` (the FIFA wire id, echoed in the
  auction record). New column; create_listing takes both; row/Listing updated.
- market.rs: `MarketCardResolver` trait; handle_market_list resolves resourceId
  -> Core card_id and fails closed (persists nothing) on an unmappable resource;
  auction_record emits `resourceId` from wire_resource_id. Dispatch passes the
  resolver.

Tests: list_unknown_resource_fails_closed_no_listing (B),
list_persists_core_card_and_wire_resource_across_reopen (C), catalog reverse
lookup; and the dispatch E2E now RESTORES the full Core+store restart
(economy_full_sequence_through_dispatch_and_restart) — the synthetic buy mints a
real reverse-mapped card_id, so Core's content preflight passes on reboot (A+D).
market 23 lib + catalog 15 + 2 integration green; clippy -D warnings + fmt clean.
2026-08-13 21:43:48 +00:00
OpenFUT Agent 884ecbba64 docs(route-authority): async bridge + dispatch wiring + real E2E landed (580d80a)
Records the AsyncBridge + classify_economy + try_handle_economy dispatch
(unrouted) and the economy_full_sequence_through_dispatch E2E, the
reqwest-blocking-in-async fix (off_runtime), and narrows Remaining to: Python
differential, host concurrency matrix, failure injection, importer, then the
from_config attachment + classifier barrier + reachability proofs. Flags the
two market-handler gaps (resourceId->card_id mapping; GET /purchased reveal
cache).
2026-08-13 21:30:56 +00:00
OpenFUT Agent 580d80a86e feat(fifa17): wire async economy handlers into the host via a runtime bridge (unrouted)
Bridges the synchronous thread-per-connection host to the async
transfer-market/pile handlers WITHOUT flipping the classifier. classify()
is untouched; production still proxies every economy route to Python. The
new dispatch is exercised only by the integration harness via
Server::try_handle_economy — handler wiring, not authority cutover.

async_bridge.rs: AsyncBridge owns ONE process-lifetime multi-threaded Tokio
runtime, shared by every connection via Arc. block_on() runs a future from
the sync dispatch thread; if invoked from within an ambient runtime it
offloads onto its own runtime + a std channel instead of panicking
("cannot start a runtime from within a runtime"). 4 unit tests incl. the
nested-runtime-safety case and concurrent multi-thread drivers.

lib.rs: EconomyRoute + classify_economy (mirrors the Python route table:
credits, purchasegroup, store/transaction, purchased, item DELETE/PUT,
ut/delete match/item/trade, auctionhouse/transfermarket, tradePile, trade).
EconomyServices (Core econ transport + durable MarketStore/PileStore + the
bridge + the pack-content pool), attached via Server::with_economy (kept out
of `new`/`from_config` so existing tests build a DB-less Server; production
from_config attachment is the barrier step). Server::try_handle_economy
dispatches: sync handlers (credits/purchasegroup/store-buy/pack-open/
quick-sell/match) inline; async handlers (market list/query/buy/cancel,
move) on the bridge via owned `async move` blocks. build_content_pool
derives the resolvable FIFA∩Core candidate pool from Core content.

market.rs: FIX the load-bearing hazard the FakeEconomy tests missed — the
async market handlers call the BLOCKING reqwest Core client, which panics
(reqwest::blocking::wait::enter) when run while a Tokio runtime is entered.
off_runtime() hops each Core call to a fresh OS thread with no runtime
entered, so blocking is legal. handle_move_items resolver gains `+ Sync`
(future must be Send for the bridge).

tests/economy_integration.rs: economy_full_sequence_through_dispatch drives
the WHOLE cluster through the REAL Server dispatch + bridge against a live
in-process Core (seeded with fifa17 dev content: 100k coins + owned cards),
on a plain OS thread (direct bridge path), over the real blocking
HttpCoreClient — no fakes: Store BUY (pool draw + shape + debit 400 + mint 5),
credits, quick-sell (reverse-resolve + credit), match WIN (+400), market
list->query->buy->query(sold)->second-buy-fails(no double debit), cancel
(cancelled not buyable), move-items, then reopen the durable market/pile
stores from disk (sold + pile persist). Deterministic. start_core_seeded
loads fifa17 dev content so /collection renders real definitions.

Tests: host 68 lib (+4 bridge) + 2 economy_integration + 24 host_test, all
green; adapter unchanged-green; clippy -D warnings + fmt clean.
2026-08-13 21:30:10 +00:00
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
OpenFUT Agent 4d2b8b9be3 economy(fifa17): land Store + Market writer handlers + pack generator (unrouted)
Implements the FIFA17 economy WRITER cluster on top of the landed Core
economy authority + host CoreEconomy client + identity/item-shaper infra.
Handlers are pub, unit-tested, and NOT yet routed: classify() and
ROUTE_AUTHORITY are untouched — the classifier barrier is a later single
coherent flip. No stubs; real Core-backed behavior; fail-closed on CoreError.

Pack generator (adapter fut/pack_content.rs):
  generate_pack_contents(&PackDef, &mut impl Rng, &[GeneratedCandidate])
  -> Vec<GeneratedCard>. Pure, seeded (deterministic), gold-tier split +
  special_chance gate as documented OPENFUT PLACEHOLDER policy (Python
  open_pack/_pack_body parity note inline). Fail-closed empty on empty pool.

Store/item writers (host economy_store.rs), matching oracle wire shapes:
  - handle_store_buy   PUT /store/transaction -> purchase_items (debit+mint N)
    -> createPackResponse; cancel/unknown/owned_only -> 200 {}; insufficient
    -> 461 {reason,credits}; CoreError -> 503.
  - handle_pack_open   POST /purchased -> owned_only consumes the unopened
    entitlement (redeem_entitlement, consume-once); normal packs debit+mint.
  - handle_quick_sell{_path,_body}  DELETE .../item/<id> + POST /ut/delete/.../item
    -> reverse-resolve wire->Core id (SquadWireResolver) -> sell_item ->
    {items:[{id}],totalCredits}; not-owned skipped.
  Production OwnedItemLookup = CoreItemLookup over CoreAccess.

Market (host market_store.rs / pile_store.rs / market.rs), synthetic-seller:
  - MarketStore over sqlx SQLite (WAL-once + busy_timeout=5s + BEGIN IMMEDIATE
    for writes, mirroring openfut-core::db). listings(active/reserved/sold/
    cancelled), owner-checked cancel, CAS reserve/complete_sale/rollback.
    Typed errors NotFound/Sold/Cancelled/WrongOwner/Conflict.
  - PileStore: durable pile/location metadata keyed by Core item id.
  - handle_market_{list,query,cancel,buy} + handle_move_items. Buy-now =
    reserve (CAS) -> balance precheck (461) -> Core purchase_item (mint+debit)
    -> complete_sale; any Core failure rolls the reservation back active.
    Two concurrent buyers -> exactly one sale + one debit.

Deps (additive): rand 0.8 (adapter+host), sqlx 0.7 sqlite/runtime-tokio (host).
Tests: adapter +7 (pack_content), host +43 (economy_store 20, market/store 23
incl two_reservers_exactly_one_wins, two_buyers_exactly_one_sale_one_debit,
state_survives_reopen, move_persists_across_reopen). All green; clippy
-D warnings clean; rustfmt clean.
2026-08-13 20:47:57 +00:00
OpenFUT Agent 0b31abe1d1 test(fifa17): run economy harness on a real multi-connection Core pool
The fresh-DB write-lock race is fixed (core fbb54ea: BEGIN IMMEDIATE writes),
so the E2E+restart harness now uses max_connections=5; 10/10 deterministic.
2026-08-13 20:20:58 +00:00
OpenFUT Agent 6b652cb0a2 chore(core): BEGIN IMMEDIATE write transactions (75b1830 -> fbb54ea) 2026-08-13 20:20:21 +00:00
OpenFUT Agent 3507c5714d feat(fifa17): add purchase_items to host CoreEconomy client
Complete the transport contract: purchase_items (atomic debit + mint N) on the
CoreEconomy trait + HttpCoreClient (POST /economy/purchase-items) + FakeEconomy.
This is the open-on-buy primitive the Store BUY handler will use (createPackResponse
returns the minted itemList). Fail-closed like the rest of the client.
2026-08-13 20:06:54 +00:00
OpenFUT Agent 4f78b9a875 test(fifa17): keep economy harness serialized; document multi-conn blocker
WAL-establish-once + busy_timeout (core 75b1830) reduced but did not eliminate a
brand-new-DB multi-connection warm-up 'database error'; the E2E+restart harness
stays on a single serialized connection for determinism, and the residual
multi-connection concurrency issue is documented as the remaining Part-T blocker.
2026-08-13 20:05:34 +00:00
OpenFUT Agent f3aebafcc7 chore(core): serialize WAL establishment (75b1830) 2026-08-13 20:03:40 +00:00
OpenFUT Agent 1df0bc4f00 test(fifa17): make economy integration harness deterministic
Serialize Core access with a single pooled connection (the harness drives Core
sequentially via the blocking client) to avoid a WAL-mode-establishment race
across connections warming up on a brand-new DB file, and raise the readiness
ceiling for heavy parallel test-binary load. 10/10 deterministic. (Fixed
alongside a real Core robustness fix: per-connection pragmas + busy_timeout,
core 0360135.)
2026-08-13 19:57:53 +00:00
OpenFUT Agent 7d5d0cff06 chore(core): sqlite busy_timeout + per-connection pragmas (bcc4f51 -> 0360135) 2026-08-13 19:55:13 +00:00
OpenFUT Agent 8d752cb0e4 test(fifa17): real host<->Core economy integration harness
Spawn Core (axum) on an ephemeral loopback port backed by a disposable temp-file
SQLite, seed a fifa17 profile via the real Core HTTP API, then drive the HOST's
REAL transport (HttpCoreClient: CoreEconomy) + handlers against it — no fakes:
credits reads Core balance; match-reward writer credits via Core grant_reward;
purchasegroup full-gen renders the owned pack from a Core entitlement (no
sentinel); userMassInfo overlay derives coins from the same Core state
(credits==massinfo==Core invariant). Restart phase reboots Core from the same
on-disk DB and proves coins + entitlements persist. Temp dir + 127.0.0.1:0 only;
no prod DB/ports/containers/.105. dev-deps: openfut-core, tokio, axum.
2026-08-13 19:50:42 +00:00
OpenFUT Agent 96e80ab293 feat(import-fifa17): seed unopenedPackIds as Core entitlements
Carry unopenedPackIds through the importer: Profile model -> Report ->
ApplyPlan. GenericImportRequest now emits entitlements[] (one definition_id
per unopened pack instance, order+duplicates preserved), which Core's import
seeds as unconsumed packs rows in the same transaction. Closes the economy
import gap so a migrated profile's unopened packs become Core entitlements
(feeding purchasegroup/credits/userMassInfo). Idempotency unchanged (import
fingerprint). +1 test; 26 pass, clippy -D warnings clean.
2026-08-13 19:38:04 +00:00
OpenFUT Agent 49c5185ae3 docs(utas-host): update economy cutover progress (readers + match writer landed)
Core API + purchase_items + entitlement import + host reader handlers
(credits/purchasegroup/userMassInfo) + match reward writer landed and tested;
classifier still unflipped (barrier pending BUY/pack-open/quick-sell item shaping,
market listing state, differential/concurrency/restart).
2026-08-13 19:32:45 +00:00