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 againstapi-spec1.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*(passtag: ''for "no filter" — required by the/v2layer). - §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). Eachextendsthe generic{ building, status }. - §1.7
lacuna.stats.serverOverview()— a typed wrapper over the staticserver_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? }) —searchingfolded into the shared block, so the archaeologyviewneeds no special type. - §2.2
Status.ServerBlock.announcement?: IntBool. - §2.3
Embassy.ViewStashResponse.stored+stash: Record<string, number>. - §2.4 / 2.5
captured_count/spy_counton the security + police-station paginated lists. - §2.6
Mapget_star_map(andget_star*) stars carryzone: stringandinfluence: 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.Shipgainscombat?,max_occupants?,estimated_travel_time?,details?: { payload?: string[] }— the app now reads the manifest fromship.details.payload. - §2.10
schemabody_statusgainszone?. - §2.11 Loose
Record<string, unknown>maps tightened (archaeologyore,distributionCenter/trade/transporterresources,network19feeds). - §2.12
Trade.viewMarket/viewMyMarketcarrytrade_count. - §3.1
Empire.CreateResponseisnumber. - §3.2
recycle.seconds_per_resourceisnumber; run-in-progress fields are optional. - §3.5
Building.upgrade.cost.*resource keys are optional (timestill required). - §3.6
Building.{upgrade,downgrade}.reasonis[number, string](schema validator widens to cover the0/nullcases). [README #4] - §3.8
viewIncomingSupplyChains.supply_chains[].from_bodyis an object on bothplanetaryCommandandstationCommand; the app's runtime string-vs-object reshape is gone. - §3.10
Status.BodyBlock.incoming_*_shipsisSpacePort.Ship[]. - §3.11 Inbox
tagsisMessageTag[]on list rows and detail alike, andtypes.Inbox.MessageTagis a named export. - §3.13 Intelligence spy
assignment/possible_assignments[].taskuse the 26-membertypes.Intelligence.Assignmentunion. - §3.14
Trade.MarketTraderow 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 intoschema. (The generatedschemaempire_statusstill has example-value garbage baked in — the RPC stores keep usingtypes.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/enemyare typednumber;app/stores/rpc/body.tsrunsint()over every one on ingest. Same pattern very likely affectsEmpireBlock.Observatory.GetProbedStarsResponse.stars[].id— typednumber;app/components/observatory/probesTab.tsxstill coerces withutil.int(star.id)beforeabandon_probe.Embassyalliance ids —id/leader_id/members[].empire_idarenumberon 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<T>() 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<BoostType, (p) => Promise<any>> |
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<unknown>) => request()) |
The ~21 lacuna.parliament.propose* responses are all typed unknown. |
app/queries/subspaceSupplyDepot.ts |
(request: () => Promise<unknown>) => request() |
Type the transmit / complete responses. |
app/queries/spacePort.ts:89,91 |
target ?? ({} as Target) / target as Target |
Let getFleetFor / its key accept `Target |
app/components/miningMinistry/platformsTab.tsx |
platform as unknown as Record<string, unknown> |
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
LacunaErrortype in the client.app/queries/unwrap.tsthrows its own;unwrap.tsandServerError.dataare bothany. 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:127bootstraps status by calling the client directly (not throughunwrap);app/components/inbox/messageBody.tsx:33callslacuna.parliament.castVoteinside a component rather than via aapp/queries/*hook.