fut::squad_projection is the ONE projector for every squad read shape.
project_squad(canonical squad + Fresh extension + owned items) -> the FIFA
17 squad wire object; user_mass_info_squad and squad_list are envelope-only
wrappers over the same output (no per-endpoint domain model).
Design guarantees exercised by tests:
- purity / no N+1: consumes a host-assembled input (read_squad_with_ext +
one batch owned-cards fetch + in-memory card defs); no per-slot lookup
- shared shaper: every occupied slot is shaped by fut::item::shape_item,
so squad items and /club items cannot drift
- Fresh -> full projection; Stale -> never applied (verdict surfaced);
Missing -> explicit, never fabricated
- captain projects as the resolved WIRE id (never resourceId); index and
formation round-trip verbatim; kit follows the player; two owned copies
of one definition stay distinct
Adds committed sanitized fixtures decoded from the squad session capture
(swap, f433, persisted userMassInfo.squad read, squad/list) and
tests/squad_projection.rs: baseline / swap / formation-change / persisted
read-after-write round-trips asserted by ownership class (canonical,
extension, shadow, derived identity), plus one-projector no-divergence.
Controlled retail capture, one criterion at a time, cleared between each.
47 transactions. Every filter the My Squad picker sends is now known from
the wire rather than guessed.
Route: GET /ut/game/fifa17/club -- the picker hits UTAS and reuses the
general club-inventory route.
level=any|gold quality lowercase, ALWAYS present
rare=SP "Special" uppercase, OMITTED when off
position=ST position uppercase, omitted when off
nation=52 entity id numeric
league=13 entity id numeric
team=5 entity id numeric, NESTED under league
sort=desc client constant; the UI has no sort control
start=/count=11 pagination
Two encoding families: short string enums, and numeric FIFA ids. The ids
must never reach Core. Filters compose as plain ANDs in one query --
string and id filters alike -- so each maps independently.
THE ROOT CAUSE IS SELF-AMPLIFYING.
club_route honours type, team and league; it never reads start, count,
level, sort or year. Because start is ignored, every page returns the
same full set, so the client concludes the page was full and asks for the
next one. One scroll produced 22 requests and 6.2 MB, stopping at
start=200 only because the client gave up -- against a filtered set of 32
items that should have been three pages.
That also explains why the bug reads as erratic rather than broken:
league=13&position=ST returns every Premier League player instead of
Premier League strikers. Plausible, wrongly sized, hard to notice.
Measured filtered sets, from the real cluttered club -- these are the
acceptance test for the fix:
unfiltered 1962
league=13 350
league=13&team=5 32
Two client behaviours worth carrying forward: the picker fires a query
per highlighted entry, not per selection (two requests for one club
pick), and parameter ORDER is not stable, so parsing must be key-value.
FIXTURE SIZE: bodies over 4 KB are truncated in the committed fixture,
with body_full_len and body_full_sha256 retained, because the same 1.1 MB
club response repeats ~25 times and its hash already proves identity.
13.3 MB -> 247 KB. The raw .ofcap keeps every byte, privately and
gitignored. Truncation is recorded per transaction so a trimmed fixture
is never mistaken for a whole response.
Audited across all three identifier surfaces -- headers, JSON bodies,
query strings -- before and after the size change: no leaks. 6/6
sanitiser mutations still killed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
24 transactions across 11 connections from a retail session: login,
hub, one pack open, two squad saves, a quick-sell, with before/after
state manifests. Raw .ofcap stays gitignored at 0600; the sanitized
corpus is committed as adapter fixtures.
TWO GAPS FOUND BY AUDITING THE OUTPUT, NOT BY TRUSTING THE SANITISER.
1. `POST /ut/auth` carries `macAddress` and `deviceId`. Session tokens
were being redacted correctly and these were not. A committed fixture
is a published fixture.
2. Then, with those fixed, the audit fired AGAIN on the file about to be
committed: `GET .../phishing/trusteddevice?deviceId=...` puts the id in
the QUERY STRING. Three input surfaces carry identifiers -- headers,
JSON bodies, and query strings -- and the sanitiser knew about two.
Both fixed in the tool rather than by editing the file, with a
regression test and a mutation for the query path.
AND A THIRD ARTEFACT MIX-UP, in the mutation harness itself. It reported
the query-redaction mutation as SURVIVED while a hand-run of the same
mutation killed it. Cause: the harness pointed at a stale scratchpad copy
of the test that pre-dated the query assertion, so it was faithfully
testing the mutated tool against a test that could not detect the
mutation. That is the same class as the build guard checking the wrong
binary and cargo reusing a binary compiled from mutated source -- the
third instance today of measuring the wrong artifact. The harness now
resolves ROOT from its own location and runs the COMMITTED test; the
stale copy is deleted.
Harness committed as scripts/mutate-utas-observe.py so this is repeatable
rather than a thing that happened once in a scratch directory. 6/6 killed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>