docs: capture the unlisted transfer-list state and model the pile/auction boundary

Q2 measured, not guessed. The operator moved a card Club -> Transfer List without
listing it (PUT /item, no POST /auctionhouse) and the state was captured read-only.

Finding: we do not represent the unlisted state on the wire AT ALL. Such an item is
byte-identical to a club item -- itemState `free`, no `pile` field emitted, still
returned in /club and counted in clubPlayers -- while an actively-listed item is
correctly excluded from /club and present in tradePile. Only the host's own pile
store knows the difference. Trade pile held 6 items: 1 listed, 5 unlisted.

The FIFA 17 ENCODING of that state stays UNKNOWN on purpose: returned itemData.pile
is numeric with an unrecovered mapping, and tradeState is a closed table walk where
an unrecognised bidState is silently swallowed as `none`, so a wrong enum produces a
plausible-looking but wrong UI. The corpus warns `inactive` decodes but no client
path treats it specially. One client-only discriminator is recorded instead.

Also models the domain boundary both limbo bugs came from: pile membership and
auction lifecycle are separate facts requiring coordinated transitions. States the
testable invariant -- an item must never be simultaneously excluded from /club and
absent from /tradePile -- with the two ways it was reachable and the commits that
closed each.
This commit is contained in:
funman300
2026-08-17 20:01:51 +00:00
parent 4e31fb98a2
commit 7f37b37be3
5 changed files with 319 additions and 2 deletions
+66
View File
@@ -330,3 +330,69 @@ pile reads `club` while the listing row stays `active`, so the card is filtered
of `/club` (exclusion keys on active listings) AND still rendered in the Transfer
List — the move silently appears to do nothing. `reserved` (mid-sale) and `sold`
rows are never touched, so a card can never be both sold and returned.
---
## Q2 — the unlisted transfer-list item, MEASURED 2026-08-17
The operator moved a card Club -> Transfer List **without listing it**
(`PUT /ut/game/fifa17/item`, no `POST /auctionhouse`). State captured immediately,
read-only. Fixture: `docs/evidence/market-lifecycle-2026-08-17/`.
Server truth at capture: trade pile held **6** items, of which **1** had an active
auction and **5** were unlisted.
| Surface | Unlisted item | Actively-listed item (contrast) |
|---|---|---|
| `/tradePile` `auctionInfo` | **absent** | present, `tradeState: active` |
| `/tradePile/counts` | **not counted** (`count 1, selling 1`) | counted |
| `/club` | **present**, `clubPlayers` unchanged at 1965 | **absent** (excluded) |
| `itemData.itemState` | `free` | `listFS` |
| `itemData.pile` | **field not emitted at all** | not emitted |
| `itemData.untradeable` | `false` | `false` |
**Finding: we do not represent the unlisted state anywhere on the wire.** Such an
item is byte-identical to a club item; only the host's own pile store knows it is in
the trade pile, and nothing the client receives says so.
Still **UNKNOWN** (do not guess): the FIFA 17 encoding of that state. The PE
documents returned `itemData.pile` as NUMERIC with an unrecovered mapping, and
`tradeState` decodes through a closed table walk (`active=1 inactive=2 expired=3
closed=4`) where an unrecognised `bidState` is silently swallowed as `none` — so a
wrong enum yields a plausible-looking but WRONG UI. The corpus explicitly warns that
`inactive` decodes but "no client path treats it specially; do not emit it".
Open discriminator, needs the client only: after backing out of FUT and re-entering,
does the Transfer List still show an unlisted item? If it does not, the move is not
durable from the client's point of view and the state must be represented; if it
does, `tradePile` is auctions-only by design and there is nothing to fix.
---
## Domain boundary: pile membership vs auction lifecycle
Two SEPARATE facts, but several mutations require a COORDINATED transition. Both
limbo bugs this session came from conflating them, so this is modelled explicitly
and covered by tests rather than patched route by route.
| Mutation | Pile | Auction |
|---|---|---|
| Club -> Transfer List | becomes `trade` | none necessarily exists |
| list item | stays `trade` | becomes `active` |
| clock runs out unsold | stays `trade` | becomes `expired` (projection; no row mutated) |
| relist | stays `trade` | `active` again, clock restarted, new prices |
| return expired item to Club | becomes `club` | any `active` association MUST end |
| sold / reserved | — | MUST NOT be ended by a generic move-to-club |
Invariant, stated so it can be tested rather than remembered: **an item must never be
simultaneously excluded from `/club` and absent from `/tradePile`.** That is the limbo
state, and it is reachable in two ways, both now closed:
1. Excluding by *pile* rather than by *active listing* (fixed in `f2c4927`) — the
pile can hold items with no auction, and `/tradePile` renders auctions only.
2. Moving to `club` while leaving the auction `active` (fixed in `4e31fb9`) — the
pile says club, the exclusion still fires, and the card shows in neither place.
Tests: `returning_an_expired_listing_to_the_club_ends_its_auction`,
`a_pile_move_never_disturbs_a_sale_in_flight`,
`club_excludes_listed_items_and_paginates_the_visible_set`.
@@ -0,0 +1,9 @@
{
"auctionCount": 1,
"clubPlayers": 1965,
"tradePile": {
"count": 1,
"selling": 1,
"sold": 0
}
}
@@ -5,7 +5,7 @@
"buyNowPrice": 15000,
"coinsProcessed": 0,
"currentBid": 0,
"expires": 2220,
"expires": 1199,
"itemData": {
"assetId": 20801,
"attributeList": [
@@ -5,7 +5,7 @@
"buyNowPrice": 15000,
"coinsProcessed": 0,
"currentBid": 0,
"expires": 2220,
"expires": 1199,
"itemData": {
"assetId": 20801,
"attributeList": [
@@ -0,0 +1,242 @@
{
"100000059": {
"assetId": 158023,
"attributeList": [
{
"index": 0,
"value": 89
},
{
"index": 1,
"value": 90
},
{
"index": 2,
"value": 86
},
{
"index": 3,
"value": 96
},
{
"index": 4,
"value": 26
},
{
"index": 5,
"value": 61
}
],
"cardassetid": 158023,
"cardsubtypeid": 0,
"contract": 7,
"definitionId": 158023,
"discardValue": 1500,
"fitness": 99,
"id": 100000059,
"itemState": "free",
"itemType": "player",
"leagueId": 53,
"nation": 52,
"owners": 1,
"playStyle": 250,
"preferredPosition": "RW",
"rareflag": 1,
"rating": 93,
"resourceId": 158023,
"teamid": 241,
"untradeable": false
},
"100000122": {
"assetId": 177003,
"attributeList": [
{
"index": 0,
"value": 74
},
{
"index": 1,
"value": 75
},
{
"index": 2,
"value": 86
},
{
"index": 3,
"value": 89
},
{
"index": 4,
"value": 72
},
{
"index": 5,
"value": 66
}
],
"cardassetid": 177003,
"cardsubtypeid": 0,
"contract": 7,
"definitionId": 177003,
"discardValue": 1500,
"fitness": 99,
"id": 100000122,
"itemState": "free",
"itemType": "player",
"leagueId": 53,
"nation": 10,
"owners": 1,
"playStyle": 250,
"preferredPosition": "CM",
"rareflag": 1,
"rating": 89,
"resourceId": 177003,
"teamid": 243,
"untradeable": false
},
"100000127": {
"assetId": 177003,
"attributeList": [
{
"index": 0,
"value": 74
},
{
"index": 1,
"value": 75
},
{
"index": 2,
"value": 86
},
{
"index": 3,
"value": 89
},
{
"index": 4,
"value": 72
},
{
"index": 5,
"value": 66
}
],
"cardassetid": 177003,
"cardsubtypeid": 0,
"contract": 7,
"definitionId": 177003,
"discardValue": 1500,
"fitness": 99,
"id": 100000127,
"itemState": "free",
"itemType": "player",
"leagueId": 53,
"nation": 10,
"owners": 1,
"playStyle": 250,
"preferredPosition": "CM",
"rareflag": 1,
"rating": 89,
"resourceId": 177003,
"teamid": 243,
"untradeable": false
},
"100004866": {
"assetId": 163824,
"attributeList": [
{
"index": 0,
"value": 70
},
{
"index": 1,
"value": 43
},
{
"index": 2,
"value": 56
},
{
"index": 3,
"value": 62
},
{
"index": 4,
"value": 82
},
{
"index": 5,
"value": 82
}
],
"cardassetid": 163824,
"cardsubtypeid": 0,
"contract": 7,
"definitionId": 163824,
"discardValue": 900,
"fitness": 99,
"id": 100004866,
"itemState": "free",
"itemType": "player",
"leagueId": 13,
"nation": 50,
"owners": 1,
"playStyle": 250,
"preferredPosition": "CB",
"rareflag": 1,
"rating": 83,
"resourceId": 163824,
"teamid": 7,
"untradeable": false
},
"100004867": {
"assetId": 172610,
"attributeList": [
{
"index": 0,
"value": 69
},
{
"index": 1,
"value": 33
},
{
"index": 2,
"value": 47
},
{
"index": 3,
"value": 57
},
{
"index": 4,
"value": 80
},
{
"index": 5,
"value": 76
}
],
"cardassetid": 172610,
"cardsubtypeid": 0,
"contract": 7,
"definitionId": 172610,
"discardValue": 900,
"fitness": 99,
"id": 100004867,
"itemState": "free",
"itemType": "player",
"leagueId": 31,
"nation": 54,
"owners": 1,
"playStyle": 250,
"preferredPosition": "CB",
"rareflag": 1,
"rating": 80,
"resourceId": 172610,
"teamid": 54,
"untradeable": false
}
}