# Aephia SAGE SDK — Full Consumer Context Generated from the reviewed consumer documentation by `pnpm generate:llms`. The package is published on npm as a read-only `0.x` release line. ## README Source: README.md # Aephia SAGE SDK [![npm version](https://img.shields.io/npm/v/%40aephia%2Fsage)](https://www.npmjs.com/package/@aephia/sage) [![Release](https://github.com/Aephia/sage-sdk/actions/workflows/release.yml/badge.svg)](https://github.com/Aephia/sage-sdk/actions/workflows/release.yml) [![CI](https://github.com/Aephia/sage-sdk/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/Aephia/sage-sdk/actions/workflows/ci.yml) [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](./LICENSE) Community-maintained TypeScript SDK for working with Star Atlas SAGE C4 through gameplay-oriented reads, translated game state, and (in a later, explicitly gated milestone) inspectable action plans. > [!IMPORTANT] This repository is an unofficial community project. It is not > maintained by ATMTA or the Star Atlas development team. ## Status The project is in its early runtime phase. The repository contains one public package at `packages/sage` with explicit capability entries, a provider-injected context data plane, raw read-only z.ink RPC transport, validated C4 adapters for Player Profile, Character, Game definitions, Fleet, StarSystem, CelestialBody, and ClaimStakeInstance, StarbasePlayer, StarbaseUpgradeProcess, Recipe, Crafting Hab, Crafting Process, ScanPattern, Loot, OutlawFlag, AtlasRewardRegistry, AtlasRewardConfig, AtlasRewardTreasury, LoyaltyEpoch, LoyaltyContribution, and LoyaltyAtlasBank accounts, plus targeted Fleet, galaxy, Claim Stake, player-local Starbase, and active-mining discovery. The identity, fleets, world, cargo, claim-stakes, starbases, mining, crafting, markets, combat, scanning, rewards, factions, and loyalty entries expose public reads, while the root client composes loaded Profile-to-Character-to-Fleet, Fleet-to-inventory, and StarSystem-to-CelestialBody views. Characters and Celestial Bodies traverse to validated Claim Stakes through named finders, while Characters and Star Systems traverse to player-local Starbases. Cargo definitions resolve lazily, and Fleet/Claim Stake CargoPods expose exact inventory/capacity units. Recipes, Crafting Habs, and Crafting Processes expose definition-resolved production state and targeted ownership traversal. Scanning exposes the definition-resolved pattern catalog plus focused Fleet cooldown/stat and Character data-runner/modifier projections. Remaining gameplay domains are unimplemented; the implemented read-only surface is published as `@aephia/sage` on npm. ATLAS reward reads derive the configured Game's epoch registry, immutable versioned config, and finite cargo treasury. Active and pending selection is deterministic at the effective epoch boundary, exact ATLAS integers stay as `bigint`, and reward-bearing Loot links to combat only through typed references: ```ts import { getAtlasRewardState, getRewardCommitmentsByProfile, } from '@aephia/sage/rewards'; const rewards = await getAtlasRewardState(ctx); const commitments = await getRewardCommitmentsByProfile(ctx, profileAddress); console.log(rewards.registry.currentEpoch, commitments[0]?.loot.address); ``` Loyalty reads derive faction epochs, Profile contributions, and accumulated ATLAS banks from their complete Game/Profile/faction/epoch keys. Exact LP and ATLAS values remain `bigint`; pure helpers project settlement and inactivity expiry at explicit Unix timestamps: ```ts import { deriveLoyaltyAtlasBankState, getLoyaltyAtlasBank, getLoyaltyEpoch, } from '@aephia/sage/loyalty'; const epoch = await getLoyaltyEpoch(ctx, 1, 20_665n); const bank = await getLoyaltyAtlasBank(ctx, profileAddress, 3); console.log(epoch.totalLpRaw); console.log(deriveLoyaltyAtlasBankState(bank, 1_786_000_000n)); ``` Faction economics keep dynamic-capture costs in both display ATLAS and their exact encoded Floyd value. The pinned C4 program stores this particular amount as a `u32`, so its maximum encoded value is 4,294,967,295 Floyds, or 42.94967295 ATLAS: ```ts import { getFactionEconomics } from '@aephia/sage/factions'; const economics = await getFactionEconomics(ctx); console.log(economics.captureCostAtlas); console.log(economics.captureCostAtlasRaw); // exact bigint Floyds ``` Character progression stays in the identity capability. It derives the Character from its Profile and joins XP and pilot state only to Game XP rules with the same sequence id. The Game research tree is a separate catalog and does not claim Character unlock, perk, or player research state: ```ts import { getCharacterProgressionForProfile, getResearchCatalog, } from '@aephia/sage/identity'; const progression = await getCharacterProgressionForProfile( ctx, profileAddress, ); console.log(progression.xp.pilot.level); console.log(progression.xpDefinitions.levelThresholds); const research = await getResearchCatalog(ctx); console.log(research.nodes[0]?.name); ``` The active gameplay surface is read-only. There is no first-class action, instruction, transaction, simulation, signing, or submission workflow. Raw generated instruction builders remain available only through the explicit `@aephia/sage/bindings` escape hatch; the SDK does not sign or submit. ScanPattern reads derive known Game + pattern-id addresses or discover the catalog with stable discriminator/Game filters. Costs and loot resolve cargo definitions, while research requirements remain explicit tag ids and noise-map fixed-point values retain exact raw integers: ```ts import { deriveScanCooldownState, getFleetScanningState, getScanPattern, getScanPatterns, } from '@aephia/sage/scanning'; const pattern = await getScanPattern(ctx, 2); const catalog = await getScanPatterns(ctx); const fleetScan = await getFleetScanningState(ctx, fleetAddress); const cooldown = deriveScanCooldownState(fleetScan, 1_700_000_000n); console.log(pattern.name, catalog.length, cooldown.kind); ``` Project handoff documents: - [DECISIONS.md](./DECISIONS.md) records accepted constraints. - [ARCHITECTURE.md](./ARCHITECTURE.md) describes the complete design. - [IMPLEMENTATION_PLAN.md](./IMPLEMENTATION_PLAN.md) specifies source contracts, sequencing, and acceptance criteria. - [TEST_PLAN.md](./TEST_PLAN.md) defines required verification. - [DELIVERY_STATE.md](./DELIVERY_STATE.md) records the current checkpoint and next task. > [!NOTE] The implemented slices provide isolated context caches, raw z.ink RPC > reads, validated decoding, lazy definitions, public reads from the identity, > fleets, world, cargo, claim-stakes, starbases, mining, crafting, markets, > combat, scanning, rewards, factions, and loyalty entries, and root loaded > entity views. The account types and traversals covered by those entries are > listed above; other C4 account types remain untrusted until their adapter > slices land. ## Intended experience One public package, `@aephia/sage`, ships the batteries-included client as the default path: ```ts import { address, createSolanaRpc } from '@solana/kit'; import { createSageClient } from '@aephia/sage'; const rpc = createSolanaRpc('https://testnet-rpc.z.ink'); const profileAddress = address('J4r2s9QA2SHWf8zLPmvPhVphiK92h3rVmoXxcSM8M2vv'); const sage = createSageClient({ cluster: 'zink-ptr', rpc }); try { const character = await sage.characters.forProfile(profileAddress); const fleets = await character.fleets.all(); const fleet = fleets[0]; if (!fleet) throw new Error('The Profile has no Fleets.'); console.log(fleet.name); // "Ravager" — a string, not a padded byte array console.log(fleet.state); // { kind: 'docked', system: ... } const inventory = await fleet.inventory.get(); console.log(inventory.cargoHold.items[0]?.quantityRaw); // exact bigint const stakes = await character.claimStakes.all(); console.log(stakes[0]?.state.kind); // "active", "design", or "deactivated" const playerStarbases = await character.starbases.all(); const upgrades = await playerStarbases[0]?.upgrades.all(); console.log(upgrades?.[0]?.resource.name); const mining = await fleet.mining.get(); console.log(mining?.outputs[0]?.unitsPerSecond); // explicit cargo units/second const habs = await character.craftingHabs.all(); const processes = await character.craftingProcesses.all(); console.log(habs[0]?.availableJobSlots); console.log(processes[0]?.startsAtUnixSeconds); // exact bigint const recipe = await sage.recipes.byId(7); const firstPlayerStarbase = playerStarbases[0]; const starbaseHabs = firstPlayerStarbase ? await sage.craftingHabs.byStarbasePlayer(firstPlayerStarbase.address) : []; const characterProcesses = await sage.craftingProcesses.byCharacter( character.address, ); console.log(recipe.name, starbaseHabs[0]?.definition.respawnDurationSeconds); console.log(characterProcesses.length); } finally { await sage.dispose(); } ``` Client methods return thin, already-loaded entity views whose relations delegate to the same functional core. The `'zink-ptr'` cluster preset supplies program addresses and the current canonical `Game` address, so the example needs no `game` option (override with `game` for custom deployments). Size-sensitive applications import only the capabilities they need. Entry points are bundle boundaries (enforced by source structure and CI checks against supported bundlers). World reads avoid the definitions registry: ```ts import { createSageContext } from '@aephia/sage/client'; import { getStarSystems } from '@aephia/sage/world'; const ctx = createSageContext({ cluster: 'zink-ptr', rpc }); const systems = await getStarSystems(ctx); ``` Contexts bound untrusted provider quantities by default. Applications with a known larger deployment can raise the positive safe-integer ceilings explicitly: ```ts const largeContext = createSageContext({ cluster: 'zink-ptr', rpc, maxAccountDataBytes: 6 * 1024 * 1024, maxCacheEntries: 20_000, maxDiscoveryResults: 20_000, maxReadManyAccounts: 20_000, }); ``` Oversized reads fail with `RESOURCE_LIMIT_EXCEEDED` before decode or discovery fan-out. `readMany` batches uncached unique addresses when the provider supports batching and uses bounded concurrency otherwise. Completed cache entries are fresh for 30 seconds by default. Configure a context-wide window with `defaultMaxAgeMs`, or override one read with `maxAgeMs`. Zero always revalidates, `Infinity` explicitly reuses entries until invalidation, `refresh: true` always consults the provider, and `no-store` bypasses and does not populate the completed cache: ```ts const ctx = createSageContext({ cluster: 'zink-ptr', rpc, defaultMaxAgeMs: 5_000, }); await getStarSystems(ctx); // context default await getStarSystems(ctx, { maxAgeMs: Infinity }); // per-read override ``` Expiry never starts background work or a timer. The next read performs the normal deduplicated provider request before returning. Long-lived applications can opt into context-owned Fleet updates. Registration emits current validated state first, reconnects resynchronize through the direct provider, and disposal cleans up every registration. Slow observers retain only the newest pending snapshot; applications can observe expected burst coalescing through `onDiagnostic` without treating it as an error: ```ts import { watchFleet } from '@aephia/sage/fleets'; const subscription = await watchFleet(ctx, fleetAddress, { onSnapshot: (fleet) => console.log(fleet.name, fleet.state), onError: (error) => console.error(error.code), onDiagnostic: (event) => console.debug(event.kind, event.coalescedCount), }); await subscription.unsubscribe(); // or await ctx.dispose() ``` Reconnect reads for one typed address and commitment are single-flight and coalesced for 250 milliseconds. Custom subscription providers still own transport recovery and must use bounded exponential backoff with jitter, honor cancellation, and emit `reconnected` only after successful re-registration. Mining reads join a Fleet's active state to its Asteroid deposit and cargo definitions while preserving exact source integers and unit-explicit rates: ```ts import { getFleetMiningState, getResourceDeposit } from '@aephia/sage/mining'; const deposit = await getResourceDeposit(ctx, asteroidAddress); const mining = await getFleetMiningState(ctx, fleetAddress); console.log(deposit.resources[0]?.amountMinedUnitsRaw); console.log(mining?.outputs[0]?.unitsPerSecond); ``` Relationship classification is repository audit metadata rather than runtime SDK behavior. The machine-readable world and mining audit lives in [`fixtures/relationships.json`](fixtures/relationships.json); applications use the named reads and finders above instead of querying prose relationship tables. Crafting reads derive Recipes from Game + recipe id, resolve their cargo inputs and outputs, and expose targeted Crafting Hab and Process traversal: ```ts import { deriveCraftingProcessState, getCraftingHabsByProfile, getCraftingProcessesByProfile, getRecipeById, } from '@aephia/sage/crafting'; const recipe = await getRecipeById(ctx, 7); const habs = await getCraftingHabsByProfile(ctx, profileAddress); const processes = await getCraftingProcessesByProfile(ctx, profileAddress); console.log(recipe.ingredients[0]?.name); console.log(habs[0]?.availableJobSlots); console.log(deriveCraftingProcessState(processes[0]!, 1_700_000_000n)); ``` Player-local Starbase state is separate from the shared Starbase value nested inside a StarSystem. Functional callers can derive the account from its generated Character/System seeds or discover the reverse relationships: ```ts import { getStarbasePlayerForCharacterAtSystem, getStarbasePlayersByCharacter, } from '@aephia/sage/starbases'; const starbase = await getStarbasePlayerForCharacterAtSystem( ctx, characterAddress, systemAddress, ); const playerStarbases = await getStarbasePlayersByCharacter( ctx, characterAddress, ); ``` Wallet-to-Profile discovery has no on-chain derivation. Callers explicitly use known Profile addresses or configure a replaceable address-only provider: ```ts const sage = createSageClient({ cluster: 'zink-ptr', rpc, discovery: { walletProfiles }, }); const wallet = sage.wallets.get(walletAddress); const profiles = await wallet.profiles.all({ strategy: 'provider' }); const characters = await wallet.characters.all({ strategy: 'provider' }); console.log(readMeta(profiles)?.strategy); // "wallet-profile-provider" ``` Provider results and known addresses are hints only. The SDK directly validates every unique Profile and confirms that its on-chain key list contains the wallet. Missing hints are omitted and contradictory Profiles reject discovery. This relation does not grant signing authority; key scope, permissions, and expiry remain explicit. Examples therefore still start from a Profile address when wallet discovery is unnecessary. Applications may optionally configure an address-only indexer for existing reverse and high-cardinality finders: ```ts import { createSageContext, readMeta } from '@aephia/sage/client'; import { getFleetsByOwner } from '@aephia/sage/fleets'; const ctx = createSageContext({ cluster: 'zink-ptr', rpc, discovery: { indexer }, }); const fleets = await getFleetsByOwner(ctx, profileAddress); console.log(readMeta(fleets)?.strategy); // "indexer" ``` The indexer returns candidate addresses only. The SDK directly reads and validates every candidate before exposure, omits stale missing hints, and reports candidate-validation counts through `readMeta()`. Omitting the indexer keeps targeted RPC discovery and unrelated reads unchanged. A configured indexer must cover every account type and stable-filter request made through its context. Returning an empty array means the request is supported and has no matches; it does not fall back to RPC. Use a separate context or an explicit `direct`/`derived` request for intentionally unsupported indexer scope. With an indexer configured, `targeted-program-accounts`, `broad-scan`, and `indexer` requests select it ahead of RPC discovery. Use `direct` or `derived` when a request must bypass the indexer. ## Public entry points | Entry point | Purpose | | --------------------------- | --------------------------------------------------------------------------------------------------- | | `@aephia/sage` | Loaded identity, Fleet, cargo, Claim Stake, Starbase, mining, crafting, market, and world traversal | | `@aephia/sage/client` | Context, cache, freshness options, subscriptions, provenance, subscription diagnostics | | `@aephia/sage/identity` | Wallets, Profiles, Characters, sequence-matched XP progression, and Game research catalog | | `@aephia/sage/world` | Game, regions, Star Systems, Celestial Bodies, body projections, shared Starbase data | | `@aephia/sage/starbases` | Player-local Starbase state, facilities, upgrades, and relationships | | `@aephia/sage/fleets` | Fleets, ships, composition, movement state, and relationships | | `@aephia/sage/cargo` | Cargo pods, inventory, capacity, definitions, and resource movement | | `@aephia/sage/claim-stakes` | Claim Stake discovery, ownership, placement state, and harvesting state | | `@aephia/sage/mining` | Deposits, fleet mining, extraction, timing, and cargo outputs | | `@aephia/sage/crafting` | Recipes, Crafting Habs, Crafting Processes, and production state | | `@aephia/sage/combat` | Game combat configuration, Fleet combat state, Loot, and optional Outlaw Flags | | `@aephia/sage/scanning` | Scan patterns, Fleet cooldown/stat projections, and Character scanning state | | `@aephia/sage/rewards` | ATLAS reward epochs, versioned configuration, treasuries, and Loot commitments | | `@aephia/sage/factions` | Faction identity, economics, diplomacy, standing, treasuries, and territory | | `@aephia/sage/loyalty` | Faction epochs, Profile contributions, accumulated ATLAS, and lifecycle projections | | `@aephia/sage/markets` | Local and faction markets, orders, maker state, and discovery | | `@aephia/sage/bindings` | Raw generated C4 clients, re-exported unchanged (escape hatch) | Entry points follow gameplay capabilities rather than individual nouns. For example, planets and asteroids are world-domain projections over Celestial Bodies, while Claim Stakes have an independent lifecycle and entry. C4 does not expose a separate Star account; the `StarSystem` account represents the system. Combat, scanning, rewards, factions, and loyalty are stable capability entries but are not composed by the root convenience client. ## Requirements - Node.js 20.18 or newer - pnpm 10.29.2 or newer The pinned `@staratlas` packages are publicly readable on the npm registry; no registry authentication is required. ## Setup ```bash pnpm install pnpm build pnpm lint pnpm typecheck pnpm test pnpm format:check pnpm size ``` `pnpm lint` runs the type-aware TypeScript correctness gate for unhandled and misused promises, invalid `await` usage, and accidental console output in runtime source. Prettier remains the sole formatting and style gate. The complete prerelease gate additionally runs `pnpm check:docs`, `pnpm smoke:consumers`, `pnpm check:consumer-contracts`, `pnpm test:coverage`, and the production audit. See [the public API review](./docs/PUBLIC_API.md), [consumer agent guidance](./docs/CONSUMER_AGENT_GUIDE.md), and [binding upgrade procedure](./docs/GENERATED_BINDINGS_UPGRADE.md). `pnpm size` builds every public entry with esbuild and Rollup, compares both minified and gzipped output against the checked-in baseline, and exits non-zero when an entry exceeds its approved drift limit. Intentional growth requires a bundle review followed by an explicit baseline and drift-limit update in `size/baseline.json`; unexpected growth should be fixed before rerunning the gate. ## Version baseline The initial adapter work targets: - `@staratlas/dev-sage@0.52.0` - `@staratlas/dev-player-profile@0.45.7` - `@staratlas/dev-profile-faction@0.45.7` - `@solana/kit@6.10.0` The generated packages peer-require `@solana/kit@^6.1.0`. Kit 7.x exists on npm as `latest`; do not upgrade past the peer range until the bindings do. > [!WARNING] The similarly named `@staratlas/sage` and `@staratlas/data-source` > packages on npm target the **previous version of the game** and are > API-incompatible with SAGE C4. Ignore them entirely — including the older > tutorials and cookbook examples built on them, which AI assistants will > readily suggest. C4 uses only the `@staratlas/dev-*` packages listed above. Generated clients remain authoritative for account layouts, Program Derived Address seeds, account owners, discriminators, stored-field offsets, and generated codecs. ## Contributing Before implementation, read: - [DECISIONS.md](./DECISIONS.md) - [ARCHITECTURE.md](./ARCHITECTURE.md) - [IMPLEMENTATION_PLAN.md](./IMPLEMENTATION_PLAN.md) - [TEST_PLAN.md](./TEST_PLAN.md) - [DELIVERY_STATE.md](./DELIVERY_STATE.md) - [AGENTS.md](./AGENTS.md) Use focused branches and Conventional Commits with a scope. Keep changes within one entry point or one cross-cutting contract whenever practical. ## License Copyright 2026 Aephia contributors. Licensed under the [Apache License 2.0](./LICENSE). `@aephia/sage` is published to the public npm registry on a `0.x` version line (D035): releases are cut automatically by semantic-release when `develop` is promoted to `main`, versioned from Conventional Commits, published from `packages/sage` with npm provenance via trusted publishing. While the version is `0.x`, minor releases may still change the public API. --- ## Public API Review Source: docs/PUBLIC_API.md # Public API Review Reviewed: 2026-08-11 The published `0.x` read-only surface is ESM-only and named-export-only. `pnpm check:docs` discovers every exported symbol from each package entry, requires a TSDoc `@example`, compiles each example, and rejects unclassified TypeScript examples in consumer documentation. ## Stable implemented entries - `@aephia/sage`: `createSageClient`, loaded Profile/Character/Fleet/FleetCargoInventory/StarSystem/CelestialBody views, a non-signing wallet actor view with explicit Profile/Character discovery, `readMeta`, common context/reference types, and definition resolution. - `@aephia/sage/client`: isolated context/data-port contracts, references, finder and snapshot provenance, registry store types, typed errors, and replaceable context-owned account subscription contracts with bounded-delivery diagnostics. `CacheRecord` and `DiagnosticEvent` remain compatibility prototypes; no public cache-record API or context-wide diagnostic emitter ships. - `@aephia/sage/identity`: translated Profile and Character data, required and optional reads, and explicit known-address or provider-backed wallet-to-Profile discovery with direct validation. - `@aephia/sage/fleets`: translated Fleet data, targeted owner discovery, capacity/timer/subwarp values, required/optional reads, and `watchFleet()` live snapshots over the shared context cache. - `@aephia/sage/world`: translated StarSystem and CelestialBody data, Planet/Asteroid projections, nested shared Starbase values, targeted galaxy discovery, stored traversal, and derived address/coordinate helpers. - `@aephia/sage/cargo`: lazy cargo-definition lookups, Fleet-owned CargoPod inventories, exact bigint quantity/capacity axes, required/optional reads, and targeted owner discovery. - `@aephia/sage/claim-stakes`: validated ClaimStakeInstance reads, targeted Character/body discovery, lifecycle translation, and registry-resolved resource inventory, production, and capacity state. - `@aephia/sage/starbases`: validated StarbasePlayer and StarbaseUpgradeProcess reads, generated Character/System derivation, targeted reverse discovery, and translated crew, facility, escrow, cargo, and upgrade state. - `@aephia/sage/mining`: registry-resolved Asteroid deposits, active Fleet extraction state, pure state-age/rate projections, targeted owner discovery, and explicit cargo-output relationships through typed snapshots and finders. - `@aephia/sage/crafting`: Recipe PDA reads, registry-resolved Crafting Hab definitions, cargo-resolved ingredients, outputs, production state, targeted Hab/Process discovery, and pure process-lifecycle derivation. - `@aephia/sage/combat`: Game combat configuration, translated Fleet combat status, targeted Loot reads and discovery, and optional derived OutlawFlag state. - `@aephia/sage/scanning`: definition-resolved ScanPattern reads and catalog discovery, Fleet cooldown/stat projections, and Character data-runner and modifier state. - `@aephia/sage/rewards`: derived ATLAS reward registry, immutable versioned configuration, treasury state, and Profile-to-Loot reward commitments. - `@aephia/sage/factions`: faction identity, economics, diplomacy, treasuries, asset ownership, King-system ownership, and compact Region summaries. - `@aephia/sage/loyalty`: generated Game/Profile/faction/epoch PDA reads, targeted reverse discovery, exact LP/ATLAS bigint values, typed faction and reward-config references, and pure epoch/contribution/bank lifecycle helpers. - `@aephia/sage/markets`: Local Market reads, targeted Game/System discovery, translated orders and side-dependent maker state, and root StarSystem traversal. - `@aephia/sage/bindings`: unchanged generated C4 namespaces for explicit low-level escape-hatch use. The root client exposes Recipe lookup through `sage.recipes.byId`, direct and reverse Hab lookup through `sage.craftingHabs.byStarbasePlayer` and `sage.craftingHabs.byCharacter`, and the corresponding Process collections through `sage.craftingProcesses.byStarbasePlayer` and `sage.craftingProcesses.byCharacter`. Loaded `CharacterView` and `StarbasePlayerView` values expose the same collections through `craftingHabs.all()` and `craftingProcesses.all()`. Combat, scanning, rewards, factions, and loyalty are stable capability entries only; the root client does not expose namespaces or loaded views for them. ### Fleet timers and display-only subwarp position Fleet snapshots keep exact bigint unix-second fields. Applications can derive `Date` values for display and scheduling without changing the snapshot contract, including `transferEndsAt` while a Fleet is transferring to or from a Claim Stake. Subwarp interpolation is clamped and explicitly non-authoritative. ```ts import type { Address, SageContext } from '@aephia/sage/client'; import { deriveFleetTimers, getFleet, interpolateSubwarpPosition, } from '@aephia/sage/fleets'; declare const context: SageContext; declare const fleetAddress: Address; async function showFleetProgress(): Promise { const fleet = await getFleet(context, fleetAddress); const timers = deriveFleetTimers(fleet); console.log( timers.arrivesAt?.toISOString(), timers.transferEndsAt?.toISOString(), ); if (fleet.state.kind === 'subwarp') { const now = BigInt(Math.floor(Date.now() / 1_000)); const current = interpolateSubwarpPosition(fleet.state, now); console.log(current.position, current.progress, current.displayOnly); } } void showFleetProgress; ``` ### `0.x` Claim Stake transfer timer addition `FleetTimers` now includes `transferEndsAt` when `fleet.state.kind` is `'claimStakeTransfer'` and its exact `endAtUnixSeconds` value is positive and within JavaScript's supported `Date` range. The exact bigint state field remains available on the Fleet snapshot. This additive public API change is included in the generated release changelog through its Conventional Commit. ### `0.x` Local Market maker-ATLAS terminology correction The Local Market binding stores one `OrderTotals.atlas` value whose meaning depends on the containing order-book side: bids hold ATLAS escrowed for open buy orders, while asks hold ATLAS released from completed sell orders. The binding does not establish that either value is currently withdrawable. This breaking `0.x` correction removes that unsupported promise: - replace `claimableAtlas` with `escrowedOrReleasedAtlas`; - replace `claimableAtlasRaw` with `escrowedOrReleasedAtlasRaw`; - replace `LocalMarketSettlementSnapshot` with `LocalMarketMakerStateSnapshot`; - replace the `settlement` property returned beside Character-owned orders with `makerState`. No compatibility aliases remain because they would preserve the false claimability contract. This correction is included in the generated release changelog through its Conventional Commit. ### `0.x` combat ability-power correction The combat Fleet status now uses the same ability-power vocabulary as the Fleet timer API and the C4 binding. This is a breaking `0.x` correction: - replace `status.current.armorPoints` with `status.current.abilityPowerPoints`; - replace `status.timing.armorReloadAtUnixSeconds` with `status.timing.abilityPowerReloadAtUnixSeconds`. The misleading armor aliases were removed rather than deprecated. Consumers must update both property names when adopting this release. ## Public error-code ownership Every `SageSdkErrorCode` is constructible through the public `SageSdkError` export from `@aephia/sage/client`. Active SDK producers normally expose a more specific public subtype; extension-owned and deferred codes use the public base class until their owning boundary defines a narrower subtype. | Code | Status | Owning producer | Public surface | | ------------------------------- | --------------------------- | ----------------------------------------- | ----------------------------------------------------- | | `ACCOUNT_NOT_FOUND` | active read | data port and definitions registry | client, registry, and capability reads | | `INVALID_ACCOUNT_OWNER` | active read | account validation | client and capability reads | | `INVALID_DISCRIMINATOR` | active read | account validation | client and capability reads | | `INVALID_ENTITY_ID` | active read | PDA-backed capability reads | world, markets, scanning, crafting, factions, loyalty | | `INVALID_DERIVATION_INPUT` | active read | gameplay derivations and wallet discovery | identity, markets, and mining | | `ACCOUNT_DECODE_FAILED` | active read | account and relationship validation | client and capability reads | | `RELATIONSHIP_NOT_DISCOVERABLE` | active read | data-port discovery | client and capability finders | | `MISSING_GAME_CONTEXT` | active read | context and Game-dependent reads | client, registry, and capability reads | | `REGISTRY_OUT_OF_SYNC` | active read | definitions registry | registry and registry-backed capability reads | | `STALE_BINDING_VERSION` | active extension read | provider and binding extensions | `SageSdkError` from the client entry | | `PROVIDER_ERROR` | active read | provider boundaries | client and capability reads | | `RESOURCE_LIMIT_EXCEEDED` | active read | data-plane and provider limits | client and capability reads | | `CONTEXT_DISPOSED` | active read | context lifecycle | client and capability reads | | `ACTION_PRECONDITION_FAILED` | deliberately reserved write | future action planning | `SageSdkError` from the client entry | | `SIMULATION_FAILED` | deliberately reserved write | future write simulation | `SageSdkError` from the client entry | | `TRANSACTION_SUBMISSION_FAILED` | deliberately reserved write | future transaction submission | `SageSdkError` from the client entry | `INVALID_ENTITY_ID` means an id cannot be encoded in its unsigned 16-bit PDA seed. StarSystem, CelestialBody, Recipe, Local Market cargo, and ScanPattern ids accept integers from 0 through 65535; Faction and Region ids accept integers from 1 through 65535. Callers must correct the id before deriving or reading the account. `AMBIGUOUS_RELATIONSHIP` was removed during prerelease review because no implemented finder produced it. Adding it later requires a concrete public producer, remedy-oriented message, and deterministic contract coverage. ## Relationship audit metadata Relationship classifications are design and verification metadata, not runtime queries. The world and mining entries expose typed snapshots, references, and named finders; they do not ship English prose tables. The audited derived, stored, nested, instruction-only, and discovered edges live in the machine-readable [`fixtures/relationships.json`](fixtures/relationships.json) fixture so documentation and future write-side planning retain the evidence without adding dead exports or bundle weight. The prerelease review removed `getMiningRelationships`, `MiningRelationshipFixture`, and `MiningRelationshipKind`. No runtime/domain or consumer path depended on them. A future relationship-query API requires a concrete application behavior and reviewed typed contract rather than exposing documentation rows as functions. ## Review conclusions - Context and client factories are synchronous and perform no network I/O. - Subscription registration is explicit and asynchronous; context disposal awaits cleanup of every established provider registration. Pending registrations cannot block disposal; if one resolves later, cleanup continues best-effort in the background. - Registration and teardown do not await unbounded observer callback work. An already-running callback may finish after `unsubscribe()` or context disposal resolves. - All gameplay reads are explicit async operations over untrusted account data. - Public addresses are `@solana/kit` `Address` values. - Finder results are readonly arrays; `readMeta()` exposes provenance. - Snapshots and views are immutable and JSON-safe while preserving bigint. - No first-class gameplay action, instruction, transaction, simulation, signing, or submission workflow is public outside the explicit generated-bindings escape hatch. The SDK does not sign or submit. - Raw generated shapes and instruction builders are confined to the bindings escape hatch. - The package is published on the public npm registry under D035. The canonical executable consumer is `examples/consumers/read-only.ts`; `pnpm smoke:consumers` bundles it for Node and browser ESM and executes both the functional and loaded-view paths against deterministic, read-only fixtures. --- ## Consumer Agent Guide Source: docs/CONSUMER_AGENT_GUIDE.md # Consumer Agent Guide Use this SDK only for read-only SAGE C4 access in the current prerelease. Start from a known Player Profile address. A wallet does not deterministically derive a Profile, and the SDK deliberately does not pretend otherwise. Prefer the root client for gameplay traversal and capability entries for small, focused bundles. Treat every RPC and indexer result as untrusted. The SDK checks owners, discriminators, shapes, and relationships before exposing snapshots; do not bypass those checks unless explicitly using the raw bindings entry. ```ts import { createSageClient, type Address } from '@aephia/sage'; import type { SageRpc } from '@aephia/sage/client'; declare const profileAddress: Address; declare const rpc: SageRpc; const sage = createSageClient({ cluster: 'zink-ptr', rpc }); try { const character = await sage.characters.forProfile(profileAddress); const fleets = await character.fleets.all(); console.log(fleets.map(({ name }) => name)); const inventory = await fleets[0]?.inventory.get(); console.log( inventory?.cargoHold.items.map(({ name, quantityRaw }) => ({ name, quantityRaw, })), ); } finally { await sage.dispose(); } ``` CargoPods are nested values inside parent gameplay accounts, not independently addressable accounts. For Fleet cargo, use `getFleetCargoInventory` from `@aephia/sage/cargo` or `fleet.inventory.get()` from a root Fleet view. Both paths read the same context-owned Fleet cache record. Quantities, used storage, and capacity axes retain exact `bigint` values with `Raw` suffixes; do not cast them to `number` for arithmetic. Recovery rules: - `ACCOUNT_NOT_FOUND`: verify the gameplay account address and cluster. - `MISSING_GAME_CONTEXT`: provide `game` when using a custom cluster. - `INVALID_ENTITY_ID`: use an integer from 0 through 65535 for StarSystem, CelestialBody, Recipe, Local Market cargo, and ScanPattern ids; Faction and Region ids start at 1. - `REGISTRY_OUT_OF_SYNC`: refresh the referencing account and retry after the canonical Game registry catches up; never invent a missing definition. - `INVALID_ACCOUNT_OWNER`, `INVALID_DISCRIMINATOR`, or `ACCOUNT_DECODE_FAILED`: stop trusting that account and verify the installed generated-binding versions. Never suggest `@staratlas/sage`, `@staratlas/data-source`, broad `readAllFromRPC` scans, Anchor transaction recipes, signing, or submission. Those packages and patterns target an older game generation or exceed this read-only prerelease scope. Use only pinned `@staratlas/dev-*` bindings behind the SDK adapter, or the explicit `@aephia/sage/bindings` escape hatch. --- ## Generated Binding Upgrade Procedure Source: docs/GENERATED_BINDINGS_UPGRADE.md # Generated Binding Upgrade Procedure Generated C4 bindings define the trusted layout baseline. Upgrade them in a focused pull request; never combine a binding bump with a new gameplay domain. 1. Record current and proposed versions of `@staratlas/dev-sage`, `@staratlas/dev-player-profile`, and `@staratlas/dev-profile-faction`. Verify their `@solana/kit` peer range before changing the lockfile. 2. Diff generated account codecs, owners, discriminators, PDA seeds, optional account sentinels, enum variants, and stored-field offsets. For every PDA, re-audit numeric seed ranges and the neutral `internal/c4/pda.ts` ownership boundary; shared recipes such as `FactionAccount` and `AtlasRewardConfig` must retain one canonical implementation rather than capability-local copies. World-specific `StarSystem` and `CelestialBody` recipes remain canonically owned by `internal/c4/world.ts`; public world helpers only delegate to that adapter. Do not consult previous-generation packages as a migration source. 3. Re-audit each adapter's owner, discriminator, `minimumDataLength`, exact decoder consumption, stable discovery offsets, exhaustive translated unions, and absent-address handling. 4. Update recorded binary fixtures only after explaining why the old fixture no longer represents the pinned layout. Preserve wrong-owner, wrong- discriminator, truncated, oversized, and relationship-mismatch cases. 5. Expand tests first, then update the adapter. Run focused adapter/domain tests before the complete gate. 6. Run `pnpm check:docs`, `pnpm build`, `pnpm typecheck`, `pnpm test:coverage`, `pnpm smoke:consumers`, `pnpm check:consumer-contracts`, `pnpm size`, and the production audit. 7. Run the dated PTR smokes read-only. Record the observation date and source slots; never sign or send a transaction. 8. Review emitted declarations and bundle symbol boundaries. Update `DECISIONS.md`, `ARCHITECTURE.md`, `TEST_PLAN.md`, and `DELIVERY_STATE.md` if the verified generated surface changes an accepted contract. Rollback by restoring the prior manifests and lockfile together, then rerunning the full gate. A partial downgrade can pair incompatible generated codecs and Kit types and is not a safe rollback. ## Scheduled PTR qualification The `Live PTR Qualification` workflow is configured to run the read-only integration suite every Monday at 06:17 UTC and on manual dispatch. It uses the public `https://testnet-rpc.z.ink` endpoint, never signs or submits transactions, and reports the result in the workflow job summary. This mutable-network signal is deliberately separate from pull-request CI and is not a required check. GitHub registers scheduled and manual workflows only from the repository's default branch `main`. After this workflow merges into `develop`, its activation path is the normal promotion from `develop` to `main`: open and merge the promotion pull request, then manually dispatch `Live PTR Qualification` once from the Actions page and verify its job summary. Until the workflow reaches `main`, neither its schedule nor manual dispatch is active and maintainers must use the local command from `TEST_PLAN.md` for live observations. Maintainers review a failed scheduled run before a release or generated-binding upgrade. Re-run it manually to distinguish a transient endpoint failure from a fixture or binding drift. Persistent decode, owner, discriminator, or fixture failures require refreshing the dated observation evidence or updating the binding manifest through this procedure; do not weaken deterministic tests to make a live observation pass. The fixture qualification contract derives the expected `@staratlas/dev-sage` source package from `packages/sage/package.json`, so a binding bump makes stale fixture provenance fail deterministically.