market: stop advertising unlisted pile members as tradeState:"inactive"

RE of the FUT front-end closed the question the Actions-panel investigation left
open, and the answer retracts Q2 rather than completing it.

`tradeState` reaches exactly ONE native branch in CardsDLL — `cmp …,0x4` at
`0x18013e619`, "is it closed?" — and `inactive`(2) and `expired`(3) take the same
edge, producing bit-identical `flagA`/`flagB` (exhaustive 22-site census of
`[reg+0x88]` reads across the PE; confirmed live, both classes read glow=0
inbox=0). The value is then handed to the movie verbatim as the Flash property
`STATE`, and the action gate lives in the APT/ActionScript FUT front-end: the
trade-pile class partitions rows with `getCardsInAuction`/`isInActiveAuction`
(traces `initPile() - IN AUCTION:` / `- NOT IN AUCTION:`) and only auction rows
reach `PreCheckCardOptions` -> `handleTradeCardAction`. A non-auction row renders
and can never be acted on, which is exactly what the operator saw.

So the rows were never usable. "LIVE-CONFIRMED" established that they RENDER,
which is not the same claim, and I treated it as if it were.

The corpus said this before any of it was built —
`plan-2026-08-06-transfer-market.md:731-733`: "`inactive` decodes but no client
path treats it specially; do not emit it." The earlier note explaining that the
warning "was written about the PRESENTATION function" was motivated reasoning.
This also fires the corpus's own pre-registered falsifier E3 (:368-373).

Removed: the `inactive` projection from `GET …/tradePile` and `…/trade/status`,
`UnlistedCandidate`, `resolve_unlisted_pile`, `unlisted_record`,
`Server::resolve_trade_pile`, and the two helpers that existed only to feed them
(`MarketStore::blocking_core_items`, `Fifa17IdentityResolver::wire_for_owned_id`).
Unlisted trade-pile membership is now internal state with no wire expression.

Nothing is stranded: `/club` excludes only items with an ACTIVE listing, so an
unlisted pile member stays visible in the club, which is where the client can act
on it. Verified live after deploy — `/tradePile` total 7 -> 1 with zero `inactive`
rows, `/trade/status` resolving only the real auction, coins unchanged at
29,843,976, and all six former rows present in `/club` (1965 items).

Tests: 126 pass, fmt + clippy clean. Two guards replace the three tests that
pinned the old behaviour: `the_trade_pile_advertises_only_real_auctions` and
`trade_status_answers_only_about_real_auctions`.

NOT fixed here, deliberately: `itemData.itemState: "listFS"` is not a FIFA 17
token (0 occurrences in CardsDLL md5 4de3493131d7d2ff7f8b360c5ac9b655, 0 in
4.26 GiB of process memory, decodes to -1; the real value is `forSale` = 5, and
the Python oracle emits `listFS` too — which is why the differential never caught
it). `CARD_OFFERSTATE` is one of three unresolved action-gate candidates and
every actionable row observed carried -1, so that change ships alone with its own
live A/B.
This commit is contained in:
funman300
2026-08-17 23:14:35 +00:00
parent 3ce69f8951
commit f9ca901a50
6 changed files with 299 additions and 467 deletions
+70 -3
View File
@@ -399,7 +399,7 @@ Tests: `returning_an_expired_listing_to_the_club_ends_its_auction`,
---
## Q2 representation — narrowed to ONE candidate by elimination (2026-08-17)
## Q2 representation — narrowed to ONE candidate by elimination (2026-08-17) — **SUPERSEDED, see the final section**
Re-entry discriminator result: **CONFIRMED BUG.** After fully leaving and re-entering
FUT, the active auction reconstructs correctly under LISTED ITEMS, but the unlisted
@@ -469,7 +469,7 @@ test, not the immediate post-mutation response.
---
## Q2 — LIVE-CONFIRMED: `tradeState: "inactive"` is the unlisted representation
## Q2 — LIVE-CONFIRMED: `tradeState: "inactive"` is the unlisted representation — **SUPERSEDED, see the final section**
Operator-observed in the real FIFA 17 client, 2026-08-17, with the bounded one-item
probe (`OPENFUT_FIFA17_UNLISTED_PROBE`). Fixture:
@@ -534,7 +534,7 @@ mixed) has been observed — it remains an open question whether FIFA 17 expects
---
## Q2 status downgrade + the "Expired" differential EXPLAINED (2026-08-17)
## Q2 status downgrade + the "Expired" differential EXPLAINED (2026-08-17) — **SUPERSEDED, see the final section**
Phase C was a PARTIAL pass. Corrected conclusions:
@@ -588,3 +588,70 @@ touched and neither is implicated. If the actions still do not appear, those rem
the next candidates — and `itemState`'s vocabulary should be dumped statically with
`fifa17-recon/tools/vocab_dump.py` (already validated against the tradeState table)
rather than guessed.
---
## Q2 — RETRACTED, and CLOSED as unrepresentable (2026-08-17, RE of the FUT front-end)
**Everything in the two Q2 sections above is superseded.** The `tradeState:"inactive"`
projection has been removed from `GET …/tradePile` and `…/trade/status`, and the
`UnlistedCandidate` machinery deleted. Live after the fix: `total` 7 → 1, only the real
auction, `inactive` rows 0, coins unchanged, and all six former rows still present in
`/club` (invariant intact).
### Why the earlier conclusion was wrong
It was `LIVE-CONFIRMED` that the row **renders**. That was never in doubt, and it is
not the same claim as the row being usable. The rows were never actionable, and could
not have been:
| Fact | Confidence |
|---|---|
| `tradeState` reaches ONE native branch, `cmp …,0x4` at `0x18013e619` ("is it `closed`?"). `inactive`(2) and `expired`(3) take the same edge → bit-identical `flagA`/`flagB`. Exhaustive 22-site census of `[reg+0x88]` reads across the PE. | CONFIRMED |
| `tradeState` is handed to the movie verbatim as the Flash property `STATE` (publisher `0x1801bf030`); the action gate is in the APT/ActionScript FUT front-end, not CardsDLL. | CONFIRMED |
| The trade-pile movie class partitions rows with `getCardsInAuction`/`isInActiveAuction` (traces `initPile() - IN AUCTION:` / `- NOT IN AUCTION:`), and only auction rows reach `PreCheckCardOptions``handleTradeCardAction`. | CONFIRMED (interned AS symbols + trace strings, live) |
| A client rule excludes non-auction pile rows from the action path (Category C). | STRONGLY SUPPORTED |
| The gate term is `STATE == 2`. | **NOT PROVEN**`STATE`, `CARD_OFFERSTATE` and the `RESERVEDPRICE`/`MAX_CREDITS` pair all differ together in every row we can observe |
| In the shipped game, unlisted pile rows were a CLIENT-SIDE transient built by the movie from a `TO_TRADEPILE` Flash message (`FUN_1801be6a0`), never server-delivered. | STRONGLY SUPPORTED |
This fires the corpus's own pre-registered falsifier E3
(`plan-2026-08-06-transfer-market.md:368-373`): the native predicates all pass
(`IS_TRADING_ENABLED=1`, `TRADE_PILE_SIZE=100`, `tradeable=1`) and an `expired` row in
the same section IS actionable, so "the Flash layer has its own gate and CardsDLL is no
longer the right place to look" is now an observation.
**The corpus said so before any of this was written**, at
`plan-2026-08-06-transfer-market.md:731-733`: "`inactive` decodes but no client path
treats it specially; **do not emit it**." The projection was built anyway, and the
earlier reasoning above (that the warning "was written about the PRESENTATION
function") was motivated rather than evidenced.
**Consequence for the product:** a server cannot put an unlisted item on the Transfer
List in an actionable state. `/tradePile` carries only twelve-atom auction records and
`pile` has no deserializer arm, so anything rendered there must be a real auction.
Unlisted trade-pile membership is now internal state with no wire expression; those
cards live in `/club`, which is where the client can act on them.
### Open: `itemData.itemState: "listFS"` is not a FIFA 17 token
Independent of the gate, and CONFIRMED: `listFS` occurs **0 times** in
`CardsDLL_Win64_retail.dll` (md5 `4de3493131d7d2ff7f8b360c5ac9b655`) and 0 times in
4.26 GiB of live process memory; it decodes to **1** through `FUN_180166660`, observed
live on our listed row. FIFA 17's value for a listed item is `forSale` (5), from the
12-row table at `0x180229cc0`. We emit `listFS` on `/tradePile` and `/trade/status`, and
so does the Python oracle (`utas_server.py:3921`) — which is exactly why the
differential never caught it: **oracle parity is necessary but not sufficient.**
Deliberately NOT fixed in the same change: `CARD_OFFERSTATE` (itemState) is one of the
three unresolved gate candidates, and every actionable row observed so far carried
`itemState = 1`. Flipping it to `forSale` could perturb the one flow that currently
works (expired → Return to Club), so it ships alone and gets its own live A/B.
### Proving the gate (not yet done)
Disassemble the AVM1 action buffer of `tradepile.isInActiveAuction` (and
`PreCheckCardOptions`): either walk from the interned symbol at `0xb84d7498` to its APT
function object in the live process, or extract
`external/ion_fut/screens/trading/tradepile` from `ui.sb` + the `cas` archives offline.
Nothing else in reach separates `STATE` from `CARD_OFFERSTATE` from the price pair,
because all three differ together in every observable row.
@@ -0,0 +1,34 @@
{
"date": "2026-08-17",
"change": "removed tradeState:'inactive' projection from /tradePile and /trade/status",
"why": "RE of the FUT front-end: the action gate is in the APT/ActionScript layer; only rows classified as auctions (getCardsInAuction/isInActiveAuction) reach PreCheckCardOptions -> handleTradeCardAction, so an 'inactive' row renders and can never be acted on. The corpus already said 'do not emit it' (plan-2026-08-06-transfer-market.md:731-733).",
"before": {
"total": 7,
"by_state": {
"expired": 1,
"inactive": 6
},
"credits": 29843976,
"trade_status_returned": 7
},
"after": {
"total": 1,
"by_state": {
"expired": 1
},
"credits": 29843976,
"trade_status_returned": 1
},
"club_invariant": {
"formerly_inactive_members_visible_in_club": 6,
"of": 6,
"stranded": 0,
"club_items": 1965
},
"not_fixed_here": {
"itemState": "listFS",
"finding": "0 occurrences in CardsDLL (md5 4de3493131d7d2ff7f8b360c5ac9b655) and 0 in 4.26 GiB of process memory; decodes to -1. FIFA 17 value is forSale (5).",
"reason_deferred": "CARD_OFFERSTATE is one of three unresolved action-gate candidates; every actionable row observed carried itemState -1, so changing it could perturb the working expired -> Return-to-Club flow. Ships alone with its own live A/B."
},
"gate_status": "Category C, STRONGLY SUPPORTED. Gate term NOT PROVEN: STATE, CARD_OFFERSTATE and RESERVEDPRICE/MAX_CREDITS all differ together in every observable row. Proof needs AVM1 disassembly of tradepile.isInActiveAuction."
}