# Using `puga` as a library (not the CLI) The `tools/*.py` scripts cover the common questions. For anything more involved — a custom what-if, a one-off cost breakdown with wages and capex, a market check the CLI doesn't expose — import the modules directly and write a short script inline. This is normal; most answers in this project's history came from a `python - <`, spec format and a worked example (Nike LST -> Deimos PP2/BSE) in `plans/chains/`. Two gotchas specific to this: (1) each base's plan must be self-contained (see above) - Nike's plan needs its OWN housing even though Deimos's doesn't compete for it; (2) a NEW base's plan always prices a fresh core module unless the founding source (e.g. a Core Module Kit) actually covers it - mark it with `cm_free: true` on that base in the chain spec (mirrors `tools/simulate.py --cm-free`), or the phantom ~200k core-module cost craters an otherwise-fine ROI. (3) each base's reported profit/day is for its WHOLE plan, not the marginal addition - for "is this new building worth it", also simulate the base without the addition and diff, same as `tools/simulate.py`'s built-vs-planned new_capex logic but applied to profit instead of capex. ## Multi-base state: storage and ships are now tagged by location Once a 2nd base exists, `empire/state/company.yaml`'s `storage` list has multiple `STORE` entries (one per base) and must be filtered by `planet` (a field `tools/state.py` now adds, from FIO's `AddressableId` matching a site's `SiteId`) - summing all `STORE` entries blindly (as `tools/buy.py` did briefly) mixes bases' inventories together. Similarly each entry in `ships` now carries a `flight` dict (`origin`, `destination`, `eta_ms`) from live `/ship/flights`, keyed to the ship via `ShipId` (not `StlFuelStoreId`, despite that looking like a plausible match - verified 2026-09-28). `tools/runway.py`'s `inbound_ships()`/`on_hand()` are the reference implementation for scoping to one base correctly; reuse them rather than re-deriving this. ## Conventions to keep - Cache TTLs matter: market data is cached ~15 min, static game data ~24h (`puga/cache.py`). Pass `refresh=True` if you just changed something in-game and need it now. - `fio` uses PascalCase ticker/field names (`MaterialTicker`, `TimeMs`); `prunplanner` uses snake_case (`material_ticker`, `time_ms`). Don't mix them in the same dict without converting. - Never call `prunplanner.request` with a write verb outside `tools/plan_push.py` — the `[PuGa]` name guard and dry-run default live there, not in the client. - One-off scripts like these are fine to throw away; if a calculation gets reused more than once or two, promote it into a function in the relevant module instead of re-deriving it in a shell heredoc each time.