# API Implementation Status _Last updated: 2026-08-30_ · `@tlecommunity/client` **1.6.0** ## What this is The app consumes **`@tlecommunity/client` v1.6.0** (private Gitea registry). The client's types are generated from `@tlecommunity/api-spec` by `openapi-typescript` (`dist/lib/types/schema.d.ts`), with hand-written convenience wrappers on top (`dist/lib/types/*.d.ts`, `dist/lib/endpoints/*.d.ts`). Earlier versions of this doc catalogued a large parallel type surface under `app/interfaces/*`, plus inline casts, `callWithSession()` string-dispatch calls and `@ts-expect-error` suppressions, each papering over a spot where the shipped client was missing a method, missing fields on a response type, or typing a field wrong. **Client 1.6.0 ("Fixes after integrating with game UI") closed almost all of them**, and the app has been updated to match — see "Resolved in client 1.6.0" below. What remains open is a short list. > The client ships its own `README.md` "Spec Discrepancies" list — now **23 numbered items, > re-verified against `api-spec` 1.4.0**. Items marked **[README #n]** below are tracked there for > the spec owner; don't re-file them. --- ## Resolved in client 1.6.0 Typed wrappers now exist for everything the app used to reach through `callWithSession()`: - **§1.1** `lacuna.blackHoleGenerator.*` (`view` / `getActionsFor` / `generateSingularity` / `subsidizeCooldown`) + `types.BlackHoleGenerator.*`. - **§1.2** `lacuna.body.getBuildable({ body_id, x, y, tag })` + `types.Body.GetBuildable*` (pass `tag: ''` for "no filter" — required by the `/v2` layer). - **§1.3–1.5** `lacuna.body.abandon` / `rename` / `setColonyNotes` + `types.Body.*`. - **§1.6** Typed per-module `view()` on every building whose window reads side-band fields (`capitol`, `development`, `distributionCenter`, `embassy`, `entertainment`, `foodReserve`, `intelligence`, `network19`, `oreStorage`, `park`, `planetaryCommand`, `spacePort`, `spaceStationLab`, `theDillonForge`, `themePark`, `wasteExchanger`, `wasteRecycling`, `blackHoleGenerator`). Each `extends` the generic `{ building, status }`. - **§1.7** `lacuna.stats.serverOverview()` — a typed wrapper over the static `server_overview.json` (no params, no session, standard `{ result } | { error }` envelope). See "Still open" for the one caveat. Response-type fixes (fields added / corrected / tightened): - **§2.1** `Building.work` (`{ seconds_remaining, start, end, searching? }`) — `searching` folded into the shared block, so the archaeology `view` needs no special type. - **§2.2** `Status.ServerBlock.announcement?: IntBool`. - **§2.3** `Embassy.ViewStashResponse.stored` + `stash: Record`. - **§2.4 / 2.5** `captured_count` / `spy_count` on the security + police-station paginated lists. - **§2.6** `Map` `get_star_map` (and `get_star*`) stars carry `zone: string` and `influence: number | null`. - **§2.7** `Parliament.GetStarsInJurisdictionResponse.stars[].id`. - **§2.8** `Body.BuildingSummary.id` — the client splices the map key onto every row (`get_buildings` / `get_buildings_resources` / `Building.build`/`upgrade`/`downgrade`/`demolish`/ `repair`), so the app no longer patches it in. - **§2.9** `SpacePort.Ship` gains `combat?`, `max_occupants?`, `estimated_travel_time?`, `details?: { payload?: string[] }` — the app now reads the manifest from `ship.details.payload`. - **§2.10** `schema` `body_status` gains `zone?`. - **§2.11** Loose `Record` maps tightened (`archaeology` ore, `distributionCenter` / `trade` / `transporter` `resources`, `network19` `feeds`). - **§2.12** `Trade.viewMarket` / `viewMyMarket` carry `trade_count`. - **§3.1** `Empire.CreateResponse` is `number`. - **§3.2** `recycle.seconds_per_resource` is `number`; run-in-progress fields are optional. - **§3.5** `Building.upgrade.cost.*` resource keys are optional (`time` still required). - **§3.6** `Building.{upgrade,downgrade}.reason` is `[number, string]` (schema validator widens to cover the `0` / `null` cases). **[README #4]** - **§3.8** `viewIncomingSupplyChains.supply_chains[].from_body` is an object on **both** `planetaryCommand` and `stationCommand`; the app's runtime string-vs-object reshape is gone. - **§3.10** `Status.BodyBlock.incoming_*_ships` is `SpacePort.Ship[]`. - **§3.11** Inbox `tags` is `MessageTag[]` on list rows and detail alike, and `types.Inbox.MessageTag` is a named export. - **§3.13** Intelligence spy `assignment` / `possible_assignments[].task` use the 26-member `types.Intelligence.Assignment` union. - **§3.14** `Trade.MarketTrade` row is `{ id, date_offered, ask, offer: string[], empire?, body? }`. **[README #11-adjacent]** - **§3.15** Named type re-exports added (`types.ServerStatus` / `EmpireStatus` / `BodyStatus` / `PlanetStatus` / `BuildingSchema` / `ServerOverview`), so consumers no longer reach into `schema`. (The generated `schema` `empire_status` still has example-value garbage baked in — the RPC stores keep using `types.Status.EmpireBlock` / `BodyBlock`.) "Do now" cleanups from the old §4 also landed: `app/interfaces` re-exports `types.ServerDate` / `EmpireName` / `IntBool`; the unused `Buildings{Upgrade,Downgrade,Demolish}{Params,Response}` and the `EmpireCreateResponse` / `StarMapStar` / `MessageTag` / per-building `*ViewResponse` local types are deleted (the survivors in `app/interfaces/building.ts` are thin aliases onto the client types). `app/lacuna.ts` uses the exported `CAPTCHA_ERROR_CODE` / `SESSION_EXPIRED_ERROR_CODE` constants instead of the literals. --- ## Still open ### network19 `restrict_coverage` — client types `1 | 0`, server has sent `"1"` **[README #3 family]** `types.Network19.ViewResponse.restrict_coverage` is `1 | 0`, but the app has seen the live server send the string `"1"`. `app/components/network19/coverageTab.tsx` keeps a `util.int(value) === 1` guard and a widened `IntBool | string` parameter. Server should send `IntBool` consistently. ### Stringified numeric response fields **[README #1 family]** The response-side mirror of README #1. The client leans on `util.fixNumbers` + api-spec 1.4.0's id-to-`integer` normalisation, but some fields still arrive as JSON strings: - **`Status.BodyBlock`** — `x`, `y`, `orbit`, `size`, `star_id`, `building_count`, `plots_available`, `num_incoming_own/ally/enemy` are typed `number`; `app/stores/rpc/body.ts` runs `int()` over every one on ingest. Same pattern very likely affects `EmpireBlock`. - **`Observatory.GetProbedStarsResponse.stars[].id`** — typed `number`; `app/components/observatory/probesTab.tsx` still coerces with `util.int(star.id)` before `abandon_probe`. - **`Embassy` alliance ids** — `id` / `leader_id` / `members[].empire_id` are `number` on the client; verify the live server actually sends numbers here (the app now compares them as numbers). A spec-wide decision is still needed: coerce server-side, or type the affected fields `number | string` and coerce in the client pipeline. ### `Body.BuildingResources` optionality is asymmetric `ore_hour?` / `ore_capacity?` / `food?` / `ore?` are optional while the other resource-rate fields are required (`dist/lib/types/body.d.ts`). Unchanged in 1.6.0. `app/components/resources/foodOreTab.tsx` guards the `ore_*` reads specifically. Confirm which rate fields are genuinely conditional and make the optionality consistent. ### Spy-training buildings' `spies` block is untyped 1.6.0 removed the client's `SpyTrainingBuilding` type and left a TODO: the four spy-training buildings (`inteltraining`, …) answer `view` with `spies: { max_points, points_per, in_training }`, a different shape from the Intelligence Ministry's. `app/interfaces/building.ts` keeps a local `SpyTrainingBuilding` (`types.Building.ViewResponse & { spies?: {...} }`) for it. ### `types.BlackHoleGenerator.BhgTarget` is narrower than the app needs The client union models only body/star-by-name and body/star-by-id and `{ x, y }`, with ids and coordinates typed `number`. The BHG target picker also aims by **zone** and by **orbit**, and feeds raw **string** inputs. `app/interfaces/building.ts` keeps a hand-written `BhgTarget` superset and `app/queries/blackHoleGenerator.ts` widens it to the client shape with one `as unknown as` at the call boundary. Also: `generateSingularity`'s params type has no `subsidize` field, though the app (and the legacy client) send `subsidize: 1`; it is passed as a structural extra. ### `Trade.viewSupplyChains` rows are missing `body` The server's `SupplyChain::get_status` sends each chain's destination as `body: { id, name, x, y, image }`, but `types.Trade.ViewSupplyChainsResponse['supply_chains']` omits it. `app/queries/trade.ts` widens the row type (`SupplyChain`) so the supply chains tab can show the destination. Add `body` to the spec's `view_supply_chains` row to retire it. ### `stats.serverOverview` — generated type is too loose `types.Stats.ServerOverview` comes straight from the OpenAPI schema, where every field is optional and several nested shapes don't match what the "General" stats sub-tabs read. `app/queries/stats.ts` routes through the typed `serverOverview()` wrapper but narrows the result (`as unknown as ServerOverview`) to the hand-written model in `app/interfaces/stats.ts`, which stays. Tighten the spec's `server_overview` schema (required fields, concrete `types` maps) to retire the local model. --- ## Remaining escape hatches Everything the app still does outside the client's types, after the 1.6.0 adoption: | Site | What | Why it stays | | -------------------------------------------------------------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `app/queries/building.ts:30` | `select: (data) => data as TBuilding` on `useBuildingViewQuery` | Inherent to the url-keyed `buildingFromUrl(url).view()` dispatch — the generic call can't know the per-module side-band shape. A typed `buildingFromUrl()` overload would remove it. | | `app/queries/blackHoleGenerator.ts:16` | `target as unknown as types.BlackHoleGenerator.BhgTarget` | `BhgTarget` too narrow — see "Still open". | | `app/queries/stats.ts:68` | `as unknown as ServerOverview` | `server_overview` schema too loose — see "Still open". | | `app/queries/trade.ts` `SupplyChain` | Row type widened with `body` | `viewSupplyChains` rows lack the destination — see "Still open". | | `app/queries/empire.ts` | `boostRequests: Record Promise>` | Replace with the typed `lacuna.empire.boost*` union once confirmed identical (README #8 says they are). | | `app/queries/parliament.ts` / `app/components/parliament/proposalTypes.ts` | `useProposeMutation((request: () => Promise) => request())` | The ~21 `lacuna.parliament.propose*` responses are all typed `unknown`. | | `app/queries/subspaceSupplyDepot.ts` | `(request: () => Promise) => request()` | Type the transmit / complete responses. | | `app/queries/spacePort.ts:89,91` | `target ?? ({} as Target)` / `target as Target` | Let `getFleetFor` / its key accept `Target | null`, or gate the query earlier. | | `app/components/miningMinistry/platformsTab.tsx` | `platform as unknown as Record` | Type `viewPlatforms` rows. | | `app/queries/changes.ts` | raw `fetch('changes.txt')` | Intentional — plain-text asset, stays out of the client. | | `app/queries/intelligence.ts` `ASSIGNMENT_TASKS` | 26-literal fallback array | Kept as runtime data for the rare spy whose `possible_assignments` the server omits; typed against `types.Intelligence.Assignment`. | --- ## Notes / non-issues - **No `LacunaError` type in the client.** `app/queries/unwrap.ts` throws its own; `unwrap.ts` and `ServerError.data` are both `any`. - **`docs/`** carries no response schemas — historical (Reflux/Gulp-era) only. - **Endpoint-union local types** (`SecurityEndpoint`, `MarketEndpoint` / `MarketListEndpoint`, `CommandEndpoint`, `RecyclingEndpoint`, …) are legitimate — they express "these modules share a method surface" and don't duplicate anything. Keep. - **Behavioural, not type gaps:** `app/game.tsx:127` bootstraps status by calling the client directly (not through `unwrap`); `app/components/inbox/messageBody.tsx:33` calls `lacuna.parliament.castVote` inside a component rather than via a `app/queries/*` hook.