Found during the PR #68 (G23) recon pass, deliberately left unfixed there because no G23 path exercised it. Split out during the 2026-09-13 tracker reconciliation.
The defect
GET /api/lists/{id} answers the {"data":{…}} envelope. Lists.get(id:) declares Request<ListDTO> and decodes a bare ListDTO.
Packages/InterlinedKit/Sources/InterlinedKit/Endpoints/ListsEndpoint.swift:46
Packages/InterlinedDomain/Sources/InterlinedDomain/Services/ListsService.swift:359
So ListsService.detail(listId:) cannot decode a live response at all — it fails on every real call.
Why nothing has broken yet
No shipped path calls it. The shared-with-me rows in G23 come from the watching payload itself, and the owned-lists surfaces use the collection routes. The method is correct-looking, tested against a fabricated bare-ListDTO fixture, and dead.
That combination is the dangerous part: the next feature that reaches for "fetch one list by id" will hit a decode failure that looks like a server problem, and the existing green test will argue it cannot be the client.
This is the same family as two shipped defects
Both were silent decode mismatches whose tests passed against invented fixtures:
- G21 link metadata — the server nests preview fields under
metadata; the flat all-optional decode succeeded to all-nil, so every timeline link rendered host-only.
- G25 org members —
OrganizationMemberDTO required userId where live rows key by id, so the roster never rendered.
Fix
Point Lists.get(id:) at the envelope, after probing the live shape read-only rather than assuming {"data":…} — this API has documented shape drift, and single-resource routes elsewhere answer {message?, <resource>}. Replace the fabricated fixture with the captured payload. Ship the BDD quartet.
Acceptance
- A live
GET /api/lists/{id} response decodes.
- The test fixture is a captured payload, not a hand-written one.
- Any other
Request<T> in ListsEndpoint decoding a bare DTO where the server sends an envelope is caught in the same sweep.
Implementation plan (added 2026-09-15) — fixed together with #85
The probe this issue asked for was done, and it found that #75 is the least consequential member of a family. The client models a list's schema as a DSL string; the API models it as an object. Every route in the family is wrong in both directions. The full evidence is in #85 and in docs/spikes/list-schema-wire-shapes.md.
GET /api/lists/{id} is confirmed exactly as described here:
GET /api/lists/33de2874-…
200 {"data":{"id":"33de2874-…","title":"New list",…,"properties":[]}}
So this is fixed in the same PR as #85 rather than on its own — splitting them would mean touching ListsEndpoint, ListsService and the fixtures twice for one root cause.
Scope carried by this issue
Lists.get(id:) → ListResponse ({message?, data}), and ListsService.detail(listId:) unwraps it.
- The same sweep across every
Request<T> in ListsEndpoint that this issue asked for. It found four more: create, update, refresh (all {message, data}) and publicList ({list, ancestors} — a third envelope convention on the same resource).
- The fabricated bare-
ListDTO fixture is replaced by the captured payload, and a test pins the regression directly: decoding the real body as a bare ListDTO must throw.
Acceptance
- ✅ A live
GET /api/lists/{id} response decodes.
- ✅ The fixture is captured, not hand-written.
- ✅ Every other
Request<T> in ListsEndpoint decoding a bare DTO against an enveloped route is caught in the same sweep.
Found during the PR #68 (G23) recon pass, deliberately left unfixed there because no G23 path exercised it. Split out during the 2026-09-13 tracker reconciliation.
The defect
GET /api/lists/{id}answers the{"data":{…}}envelope.Lists.get(id:)declaresRequest<ListDTO>and decodes a bareListDTO.Packages/InterlinedKit/Sources/InterlinedKit/Endpoints/ListsEndpoint.swift:46Packages/InterlinedDomain/Sources/InterlinedDomain/Services/ListsService.swift:359So
ListsService.detail(listId:)cannot decode a live response at all — it fails on every real call.Why nothing has broken yet
No shipped path calls it. The shared-with-me rows in G23 come from the
watchingpayload itself, and the owned-lists surfaces use the collection routes. The method is correct-looking, tested against a fabricated bare-ListDTOfixture, and dead.That combination is the dangerous part: the next feature that reaches for "fetch one list by id" will hit a decode failure that looks like a server problem, and the existing green test will argue it cannot be the client.
This is the same family as two shipped defects
Both were silent decode mismatches whose tests passed against invented fixtures:
metadata; the flat all-optional decode succeeded to all-nil, so every timeline link rendered host-only.OrganizationMemberDTOrequireduserIdwhere live rows key byid, so the roster never rendered.Fix
Point
Lists.get(id:)at the envelope, after probing the live shape read-only rather than assuming{"data":…}— this API has documented shape drift, and single-resource routes elsewhere answer{message?, <resource>}. Replace the fabricated fixture with the captured payload. Ship the BDD quartet.Acceptance
GET /api/lists/{id}response decodes.Request<T>inListsEndpointdecoding a bare DTO where the server sends an envelope is caught in the same sweep.Implementation plan (added 2026-09-15) — fixed together with #85
The probe this issue asked for was done, and it found that #75 is the least consequential member of a family. The client models a list's schema as a DSL string; the API models it as an object. Every route in the family is wrong in both directions. The full evidence is in #85 and in
docs/spikes/list-schema-wire-shapes.md.GET /api/lists/{id}is confirmed exactly as described here:So this is fixed in the same PR as #85 rather than on its own — splitting them would mean touching
ListsEndpoint,ListsServiceand the fixtures twice for one root cause.Scope carried by this issue
Lists.get(id:)→ListResponse({message?, data}), andListsService.detail(listId:)unwraps it.Request<T>inListsEndpointthat this issue asked for. It found four more:create,update,refresh(all{message, data}) andpublicList({list, ancestors}— a third envelope convention on the same resource).ListDTOfixture is replaced by the captured payload, and a test pins the regression directly: decoding the real body as a bareListDTOmust throw.Acceptance
GET /api/lists/{id}response decodes.Request<T>inListsEndpointdecoding a bare DTO against an enveloped route is caught in the same sweep.