@aephia/atlas-kit 0.4.0 → 0.6.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/README.md +49 -29
  2. package/dist/cargo/index.d.ts +1 -0
  3. package/dist/cargo/index.d.ts.map +1 -1
  4. package/dist/cargo/index.js +1 -0
  5. package/dist/cargo/index.js.map +1 -1
  6. package/dist/claim-stakes/actions/index.d.ts +393 -0
  7. package/dist/claim-stakes/actions/index.d.ts.map +1 -0
  8. package/dist/claim-stakes/actions/index.js +1747 -0
  9. package/dist/claim-stakes/actions/index.js.map +1 -0
  10. package/dist/claim-stakes/index.d.ts +64 -0
  11. package/dist/claim-stakes/index.d.ts.map +1 -1
  12. package/dist/claim-stakes/index.js +49 -0
  13. package/dist/claim-stakes/index.js.map +1 -1
  14. package/dist/client/context.d.ts.map +1 -1
  15. package/dist/client/context.js +2 -1
  16. package/dist/client/context.js.map +1 -1
  17. package/dist/client/errors.d.ts +12 -3
  18. package/dist/client/errors.d.ts.map +1 -1
  19. package/dist/client/errors.js +11 -0
  20. package/dist/client/errors.js.map +1 -1
  21. package/dist/client/index.d.ts +2 -1
  22. package/dist/client/index.d.ts.map +1 -1
  23. package/dist/client/index.js +1 -0
  24. package/dist/client/index.js.map +1 -1
  25. package/dist/client/lifecycle.d.ts +2 -0
  26. package/dist/client/lifecycle.d.ts.map +1 -1
  27. package/dist/client/lifecycle.js +31 -0
  28. package/dist/client/lifecycle.js.map +1 -1
  29. package/dist/combat/actions/index.d.ts +239 -0
  30. package/dist/combat/actions/index.d.ts.map +1 -0
  31. package/dist/combat/actions/index.js +1146 -0
  32. package/dist/combat/actions/index.js.map +1 -0
  33. package/dist/crafting/actions/index.d.ts +140 -0
  34. package/dist/crafting/actions/index.d.ts.map +1 -0
  35. package/dist/crafting/actions/index.js +728 -0
  36. package/dist/crafting/actions/index.js.map +1 -0
  37. package/dist/crafting/index.d.ts +74 -8
  38. package/dist/crafting/index.d.ts.map +1 -1
  39. package/dist/crafting/index.js +55 -9
  40. package/dist/crafting/index.js.map +1 -1
  41. package/dist/fleets/actions/index.d.ts +5 -4
  42. package/dist/fleets/actions/index.d.ts.map +1 -1
  43. package/dist/fleets/actions/index.js +85 -58
  44. package/dist/fleets/actions/index.js.map +1 -1
  45. package/dist/fleets/index.d.ts +1 -0
  46. package/dist/fleets/index.d.ts.map +1 -1
  47. package/dist/fleets/index.js +1 -0
  48. package/dist/fleets/index.js.map +1 -1
  49. package/dist/index.d.ts +13 -2
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +9 -2
  52. package/dist/index.js.map +1 -1
  53. package/dist/internal/c4/claim-stake-building-definitions.d.ts +50 -0
  54. package/dist/internal/c4/claim-stake-building-definitions.d.ts.map +1 -0
  55. package/dist/internal/c4/claim-stake-building-definitions.js +56 -0
  56. package/dist/internal/c4/claim-stake-building-definitions.js.map +1 -0
  57. package/dist/internal/c4/claim-stake-configuration.d.ts +17 -0
  58. package/dist/internal/c4/claim-stake-configuration.d.ts.map +1 -0
  59. package/dist/internal/c4/claim-stake-configuration.js +47 -0
  60. package/dist/internal/c4/claim-stake-configuration.js.map +1 -0
  61. package/dist/internal/c4/claim-stake-definitions.d.ts +37 -0
  62. package/dist/internal/c4/claim-stake-definitions.d.ts.map +1 -0
  63. package/dist/internal/c4/claim-stake-definitions.js +77 -0
  64. package/dist/internal/c4/claim-stake-definitions.js.map +1 -0
  65. package/dist/internal/c4/fleet.d.ts.map +1 -1
  66. package/dist/internal/c4/fleet.js +2 -102
  67. package/dist/internal/c4/fleet.js.map +1 -1
  68. package/dist/internal/c4/game-definition-layout.d.ts +11 -0
  69. package/dist/internal/c4/game-definition-layout.d.ts.map +1 -0
  70. package/dist/internal/c4/game-definition-layout.js +32 -0
  71. package/dist/internal/c4/game-definition-layout.js.map +1 -0
  72. package/dist/internal/c4/game.d.ts +13 -1
  73. package/dist/internal/c4/game.d.ts.map +1 -1
  74. package/dist/internal/c4/game.js +19 -37
  75. package/dist/internal/c4/game.js.map +1 -1
  76. package/dist/internal/c4/profile-faction.d.ts +6 -0
  77. package/dist/internal/c4/profile-faction.d.ts.map +1 -1
  78. package/dist/internal/c4/profile-faction.js +22 -1
  79. package/dist/internal/c4/profile-faction.js.map +1 -1
  80. package/dist/internal/c4/ship-stats.d.ts +5 -0
  81. package/dist/internal/c4/ship-stats.d.ts.map +1 -0
  82. package/dist/internal/c4/ship-stats.js +106 -0
  83. package/dist/internal/c4/ship-stats.js.map +1 -0
  84. package/dist/internal/c4/world.d.ts +23 -1
  85. package/dist/internal/c4/world.d.ts.map +1 -1
  86. package/dist/internal/c4/world.js +53 -1
  87. package/dist/internal/c4/world.js.map +1 -1
  88. package/dist/internal/crafting-process-state.d.ts +15 -0
  89. package/dist/internal/crafting-process-state.d.ts.map +1 -0
  90. package/dist/internal/crafting-process-state.js +12 -0
  91. package/dist/internal/crafting-process-state.js.map +1 -0
  92. package/dist/internal/planning/authorization-adapter.d.ts +74 -0
  93. package/dist/internal/planning/authorization-adapter.d.ts.map +1 -1
  94. package/dist/internal/planning/authorization-adapter.js +382 -0
  95. package/dist/internal/planning/authorization-adapter.js.map +1 -1
  96. package/dist/internal/read-meta.d.ts +39 -3
  97. package/dist/internal/read-meta.d.ts.map +1 -1
  98. package/dist/internal/read-meta.js +0 -10
  99. package/dist/internal/read-meta.js.map +1 -1
  100. package/dist/loyalty/actions/index.d.ts +40 -0
  101. package/dist/loyalty/actions/index.d.ts.map +1 -0
  102. package/dist/loyalty/actions/index.js +159 -0
  103. package/dist/loyalty/actions/index.js.map +1 -0
  104. package/dist/markets/actions/index.d.ts +100 -0
  105. package/dist/markets/actions/index.d.ts.map +1 -0
  106. package/dist/markets/actions/index.js +417 -0
  107. package/dist/markets/actions/index.js.map +1 -0
  108. package/dist/planning/assembly.d.ts +4 -0
  109. package/dist/planning/assembly.d.ts.map +1 -1
  110. package/dist/planning/assembly.js +13 -0
  111. package/dist/planning/assembly.js.map +1 -1
  112. package/dist/planning/composition.d.ts.map +1 -1
  113. package/dist/planning/composition.js +21 -2
  114. package/dist/planning/composition.js.map +1 -1
  115. package/dist/planning/execution.d.ts.map +1 -1
  116. package/dist/planning/execution.js +7 -4
  117. package/dist/planning/execution.js.map +1 -1
  118. package/dist/planning/index.d.ts +27 -8
  119. package/dist/planning/index.d.ts.map +1 -1
  120. package/dist/planning/index.js +57 -13
  121. package/dist/planning/index.js.map +1 -1
  122. package/dist/planning/plan-sequence-execution.d.ts +26 -4
  123. package/dist/planning/plan-sequence-execution.d.ts.map +1 -1
  124. package/dist/planning/plan-sequence-execution.js +26 -4
  125. package/dist/planning/plan-sequence-execution.js.map +1 -1
  126. package/dist/planning/plan-sequence.d.ts +20 -3
  127. package/dist/planning/plan-sequence.d.ts.map +1 -1
  128. package/dist/planning/plan-sequence.js +20 -3
  129. package/dist/planning/plan-sequence.js.map +1 -1
  130. package/dist/planning/simulation.d.ts.map +1 -1
  131. package/dist/planning/simulation.js +2 -1
  132. package/dist/planning/simulation.js.map +1 -1
  133. package/dist/registry/cargo.d.ts +10 -0
  134. package/dist/registry/cargo.d.ts.map +1 -0
  135. package/dist/registry/cargo.js +15 -0
  136. package/dist/registry/cargo.js.map +1 -0
  137. package/dist/registry/claim-stakes.d.ts +11 -0
  138. package/dist/registry/claim-stakes.d.ts.map +1 -0
  139. package/dist/registry/claim-stakes.js +15 -0
  140. package/dist/registry/claim-stakes.js.map +1 -0
  141. package/dist/registry/definition-section-snapshot.d.ts +42 -0
  142. package/dist/registry/definition-section-snapshot.d.ts.map +1 -0
  143. package/dist/registry/definition-section-snapshot.js +214 -0
  144. package/dist/registry/definition-section-snapshot.js.map +1 -0
  145. package/dist/registry/index.d.ts +114 -7
  146. package/dist/registry/index.d.ts.map +1 -1
  147. package/dist/registry/index.js +372 -101
  148. package/dist/registry/index.js.map +1 -1
  149. package/dist/registry/publication.d.ts +8 -0
  150. package/dist/registry/publication.d.ts.map +1 -0
  151. package/dist/registry/publication.js +10 -0
  152. package/dist/registry/publication.js.map +1 -0
  153. package/dist/registry/ship-stats-snapshot.d.ts +8 -0
  154. package/dist/registry/ship-stats-snapshot.d.ts.map +1 -0
  155. package/dist/registry/ship-stats-snapshot.js +154 -0
  156. package/dist/registry/ship-stats-snapshot.js.map +1 -0
  157. package/dist/world/index.d.ts +22 -1
  158. package/dist/world/index.d.ts.map +1 -1
  159. package/dist/world/index.js +24 -1
  160. package/dist/world/index.js.map +1 -1
  161. package/package.json +21 -1
@@ -0,0 +1,728 @@
1
+ import { getAddressEncoder, getBytesEncoder, getProgramDerivedAddress, isAddress, } from '@solana/kit';
2
+ import { InvalidActionInputError, SageSdkError } from '../../client/errors.js';
3
+ import { readContextRawAccounts } from '../../client/reconciliation.js';
4
+ import { sage } from '../../internal/c4/bindings.js';
5
+ import { craftingHabAccountDefinition, craftingProcessAccountDefinition, deriveRecipeAddress, recipeAccountDefinition, } from '../../internal/c4/crafting.js';
6
+ import { characterAccountDefinition, findCharacterPda, } from '../../internal/c4/identity.js';
7
+ import { deriveStarbasePlayerAddress, starbasePlayerAccountDefinition, } from '../../internal/c4/starbase.js';
8
+ import { deriveInternalCraftingProcessState } from '../../internal/crafting-process-state.js';
9
+ import { requireGameAddress } from '../../internal/game-context.js';
10
+ import { adaptCancelCraftingProcessInstruction, adaptStartCraftingProcessInstruction, adaptCompleteCraftingProcessInstruction, validatePlanAuthorization, } from '../../internal/planning/authorization-adapter.js';
11
+ import { deriveStarSystemAddress, starSystemAccountDefinition, } from '../../internal/c4/world.js';
12
+ import { createPlan, } from '../../planning/index.js';
13
+ const U64_MAX = (1n << 64n) - 1n;
14
+ const U32_MAX = 4_294_967_295;
15
+ function invalid(field, value, acceptedRange, remedy) {
16
+ throw new InvalidActionInputError(field, value, acceptedRange, remedy);
17
+ }
18
+ function readRecord(value, field) {
19
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
20
+ return invalid(field, value, 'a plain object', `Provide ${field} as the documented object before retrying.`);
21
+ }
22
+ return value;
23
+ }
24
+ function assertKeys(value, field, required, optional = []) {
25
+ const accepted = new Set([...required, ...optional]);
26
+ const missing = required.find((key) => !Object.hasOwn(value, key));
27
+ const unexpected = Object.keys(value).find((key) => !accepted.has(key));
28
+ if (missing !== undefined || unexpected !== undefined) {
29
+ invalid(field, missing === undefined ? unexpected : `missing ${missing}`, `exactly the fields ${[...required, ...optional].join(', ')}`, `Remove unknown fields and provide every required ${field} field before retrying.`);
30
+ }
31
+ }
32
+ function readAddress(value, field) {
33
+ if (typeof value !== 'string' || !isAddress(value)) {
34
+ return invalid(field, value, 'one valid z.ink/Solana account address', `Set ${field} to the documented account address before retrying.`);
35
+ }
36
+ return value;
37
+ }
38
+ function validateStartOptions(value) {
39
+ const options = readRecord(value, 'options');
40
+ assertKeys(options, 'options', ['authorization', 'craftingProcess', 'quantity', 'numCrew'], ['craftingHab']);
41
+ const authorizationRecord = readRecord(options.authorization, 'authorization');
42
+ assertKeys(authorizationRecord, 'authorization', ['profile', 'authority', 'keyIndex'], ['certificate']);
43
+ const authorization = authorizationRecord;
44
+ readAddress(authorization.profile, 'authorization.profile');
45
+ readAddress(authorization.authority, 'authorization.authority');
46
+ if (authorization.certificate !== undefined) {
47
+ readAddress(authorization.certificate, 'authorization.certificate');
48
+ }
49
+ validatePlanAuthorization(authorization);
50
+ if (typeof options.quantity !== 'bigint' ||
51
+ options.quantity <= 0n ||
52
+ options.quantity > U64_MAX) {
53
+ invalid('quantity', options.quantity, `a positive raw bigint from 1 through ${String(U64_MAX)} (unsigned u64)`, 'Set quantity to the exact positive Recipe quantity within the instruction codec range.');
54
+ }
55
+ if (typeof options.numCrew !== 'number' ||
56
+ !Number.isInteger(options.numCrew) ||
57
+ options.numCrew < 0 ||
58
+ options.numCrew > U32_MAX) {
59
+ invalid('numCrew', options.numCrew, `an integer from 0 through ${String(U32_MAX)} (unsigned u32)`, 'Set numCrew to the exact available crew count within the instruction codec range.');
60
+ }
61
+ return {
62
+ authorization,
63
+ craftingProcess: readAddress(options.craftingProcess, 'craftingProcess'),
64
+ numCrew: options.numCrew,
65
+ quantity: options.quantity,
66
+ ...(options.craftingHab === undefined
67
+ ? {}
68
+ : { craftingHab: options.craftingHab }),
69
+ };
70
+ }
71
+ function validateCompleteOptions(value) {
72
+ const options = readRecord(value, 'options');
73
+ assertKeys(options, 'options', ['authorization', 'atUnixSeconds']);
74
+ const authorizationRecord = readRecord(options.authorization, 'authorization');
75
+ assertKeys(authorizationRecord, 'authorization', ['profile', 'authority', 'keyIndex'], ['certificate']);
76
+ const authorization = authorizationRecord;
77
+ readAddress(authorization.profile, 'authorization.profile');
78
+ readAddress(authorization.authority, 'authorization.authority');
79
+ if (authorization.certificate !== undefined) {
80
+ readAddress(authorization.certificate, 'authorization.certificate');
81
+ }
82
+ validatePlanAuthorization(authorization);
83
+ if (typeof options.atUnixSeconds !== 'bigint' || options.atUnixSeconds < 0n) {
84
+ invalid('atUnixSeconds', options.atUnixSeconds, 'a non-negative bigint Unix timestamp', 'Set atUnixSeconds to the explicit current Unix timestamp before retrying.');
85
+ }
86
+ return { atUnixSeconds: options.atUnixSeconds, authorization };
87
+ }
88
+ function validateCancelOptions(value) {
89
+ const options = readRecord(value, 'options');
90
+ assertKeys(options, 'options', ['authorization', 'atUnixSeconds']);
91
+ const authorizationRecord = readRecord(options.authorization, 'authorization');
92
+ assertKeys(authorizationRecord, 'authorization', ['profile', 'authority', 'keyIndex'], ['certificate']);
93
+ const authorization = authorizationRecord;
94
+ readAddress(authorization.profile, 'authorization.profile');
95
+ readAddress(authorization.authority, 'authorization.authority');
96
+ if (authorization.certificate !== undefined) {
97
+ readAddress(authorization.certificate, 'authorization.certificate');
98
+ }
99
+ validatePlanAuthorization(authorization);
100
+ if (typeof options.atUnixSeconds !== 'bigint' || options.atUnixSeconds < 0n) {
101
+ invalid('atUnixSeconds', options.atUnixSeconds, 'a non-negative bigint Unix timestamp', 'Set atUnixSeconds to the explicit current Unix timestamp before retrying.');
102
+ }
103
+ return { atUnixSeconds: options.atUnixSeconds, authorization };
104
+ }
105
+ function fail(message, address) {
106
+ throw new SageSdkError('ACTION_PRECONDITION_FAILED', message, { address });
107
+ }
108
+ function requireAccount(account, address, message) {
109
+ return account ?? fail(message, address);
110
+ }
111
+ function balance(items, cargoId) {
112
+ return items.find((item) => item.id === cargoId)?.quantityRaw ?? 0n;
113
+ }
114
+ function sameBalances(snapshot, account) {
115
+ if (snapshot.length !== account.length)
116
+ return false;
117
+ const seen = new Set();
118
+ return snapshot.every((item) => {
119
+ if (seen.has(item.id))
120
+ return false;
121
+ seen.add(item.id);
122
+ return account.some((amount) => amount.cargoId === item.id && amount.quantityRaw === item.quantityRaw);
123
+ });
124
+ }
125
+ function sameStarbaseBalances(snapshot, account) {
126
+ return sameBalances(snapshot, account.map(({ amount, cargoId }) => ({ cargoId, quantityRaw: amount })));
127
+ }
128
+ function validateKnownInputs(recipe, items, quantity, source, sourceAddress) {
129
+ for (const ingredient of recipe.ingredients) {
130
+ const required = ingredient.quantityRaw * quantity;
131
+ const available = balance(items, ingredient.id);
132
+ if (available < required) {
133
+ fail(`${source} ${sourceAddress} ${ingredient.name} has ${String(available)} raw units, but Recipe ${recipe.name} requires ${String(required)} for quantity ${String(quantity)}. Add the missing input cargo, refresh the ${source}, and retry.`, sourceAddress);
134
+ }
135
+ }
136
+ }
137
+ async function deriveCurrencyCacheAddress(game) {
138
+ const [address] = await getProgramDerivedAddress({
139
+ programAddress: sage.SAGE_PROGRAM_ADDRESS,
140
+ seeds: [
141
+ getBytesEncoder().encode(new TextEncoder().encode('CurrencyConfig')),
142
+ getAddressEncoder().encode(game),
143
+ ],
144
+ });
145
+ return address;
146
+ }
147
+ function validateCurrencyCache(account, game, canonicalAddress) {
148
+ const decoder = sage.getCurrencyConfigCacheDecoder();
149
+ const validDiscriminator = sage.CURRENCY_CONFIG_CACHE_DISCRIMINATOR.every((byte, index) => account.data[index] === byte);
150
+ if (account.address !== canonicalAddress ||
151
+ account.owner !== sage.SAGE_PROGRAM_ADDRESS ||
152
+ account.data.length !== decoder.fixedSize ||
153
+ !validDiscriminator ||
154
+ decoder.decode(account.data).gameId !== game) {
155
+ fail(`CurrencyCache ${account.address} does not match the configured Game, canonical PDA, owner, or pinned account layout. Refresh the Game currency configuration and retry.`, account.address);
156
+ }
157
+ }
158
+ async function hashAccountState(account) {
159
+ const owner = getAddressEncoder().encode(account.owner);
160
+ const canonical = new Uint8Array(owner.length + account.data.length);
161
+ canonical.set(owner);
162
+ canonical.set(account.data, owner.length);
163
+ const digest = new Uint8Array(await globalThis.crypto.subtle.digest('SHA-256', canonical));
164
+ return [...digest].map((byte) => byte.toString(16).padStart(2, '0')).join('');
165
+ }
166
+ async function precondition(account, describes) {
167
+ return Object.freeze({
168
+ address: account.address,
169
+ dataHash: await hashAccountState(account),
170
+ describes,
171
+ kind: 'account-state',
172
+ });
173
+ }
174
+ async function resolveAccounts(ctx, recipe, player, options, game) {
175
+ const character = await findCharacterPda(options.authorization.profile, game);
176
+ const currencyCache = options.craftingHab === undefined
177
+ ? undefined
178
+ : await deriveCurrencyCacheAddress(game);
179
+ const addresses = [
180
+ character,
181
+ player.address,
182
+ recipe.address,
183
+ options.craftingProcess,
184
+ ...(options.craftingHab === undefined
185
+ ? []
186
+ : [options.craftingHab.address, currencyCache]),
187
+ ];
188
+ const reads = await readContextRawAccounts(ctx, addresses);
189
+ const characterAccount = requireAccount(reads[0], character, `Canonical Character ${character} is missing for Profile ${options.authorization.profile}. Create or repair the Character in the configured Game, refresh the Starbase Player, and retry.`);
190
+ const playerAccount = requireAccount(reads[1], player.address, `Starbase Player ${player.address} is missing. Refresh the Starbase Player and retry starting the Crafting Process.`);
191
+ const recipeAccount = requireAccount(reads[2], recipe.address, `Recipe ${recipe.address} is missing. Refresh the Recipe and retry starting the Crafting Process.`);
192
+ if (reads[3] !== undefined) {
193
+ fail(`Crafting Process ${options.craftingProcess} already exists. Choose a new signer address for the new Crafting Process and retry.`, options.craftingProcess);
194
+ }
195
+ const characterData = characterAccountDefinition.decode(characterAccount);
196
+ if (characterAccount.address !== character ||
197
+ characterData.playerProfile !== options.authorization.profile ||
198
+ characterData.game !== game) {
199
+ fail(`The canonical Character does not match Profile ${options.authorization.profile} and the configured Game ${game}. Refresh or repair the Character relationship before retrying.`, characterAccount.address);
200
+ }
201
+ const playerData = starbasePlayerAccountDefinition.decode(playerAccount);
202
+ const canonicalPlayer = await deriveStarbasePlayerAddress(playerData.system, playerData.character);
203
+ if (playerAccount.address !== player.address ||
204
+ canonicalPlayer !== player.address ||
205
+ playerData.playerProfile !== options.authorization.profile ||
206
+ playerData.system !== player.system.address ||
207
+ playerData.character !== character ||
208
+ Math.max(0, playerData.totalCrew - playerData.busyCrew) !==
209
+ player.availableCrew ||
210
+ !sameStarbaseBalances(player.cargo.items, playerData.cargo.amounts)) {
211
+ fail(`Starbase Player ${player.address} changed or does not match its canonical System, Character, Profile, crew, or cargo relationships. Refresh the Starbase Player through this context and retry.`, player.address);
212
+ }
213
+ const recipeData = recipeAccountDefinition.decode(recipeAccount);
214
+ const canonicalRecipe = await deriveRecipeAddress(game, recipeData.id);
215
+ if (recipeAccount.address !== recipe.address ||
216
+ canonicalRecipe !== recipe.address ||
217
+ recipeData.game !== game ||
218
+ recipeData.id !== recipe.id ||
219
+ recipeData.status !== recipe.status ||
220
+ recipeData.habRequired !== recipe.habRequired ||
221
+ recipeData.usageCountRaw !== recipe.usageCountRaw ||
222
+ recipeData.usageLimitRaw !== recipe.usageLimitRaw ||
223
+ !sameBalances(recipe.ingredients, recipeData.inputs)) {
224
+ fail(`Recipe ${recipe.address} changed or does not match the configured Game, canonical PDA, status, limits, or ingredients. Refresh the Recipe through this context and retry.`, recipe.address);
225
+ }
226
+ if (options.craftingHab === undefined) {
227
+ return { character, characterAccount, playerAccount, recipeAccount };
228
+ }
229
+ const habAccount = requireAccount(reads[4], options.craftingHab.address, `Crafting Hab ${options.craftingHab.address} is missing. Refresh the Hab and retry starting the Crafting Process.`);
230
+ const currencyCacheAccount = requireAccount(reads[5], currencyCache, `Canonical CurrencyCache ${currencyCache} is missing for the configured Game. Refresh the Game currency configuration and retry.`);
231
+ const habData = craftingHabAccountDefinition.decode(habAccount);
232
+ if (habAccount.address !== options.craftingHab.address ||
233
+ habData.game !== game ||
234
+ habData.ownerProfile !== options.authorization.profile ||
235
+ habData.system !== player.system.address ||
236
+ habData.state.kind !== options.craftingHab.state.kind ||
237
+ Math.max(0, habData.jobCapacity - habData.jobsRunning) !==
238
+ options.craftingHab.availableJobSlots ||
239
+ !habData.unlockedRecipeIds.includes(recipe.id) ||
240
+ !sameBalances(options.craftingHab.production.inventory.items, habData.production.inventory.amounts)) {
241
+ fail(`Crafting Hab ${options.craftingHab.address} changed or does not match its Game, Profile, System, active capacity, unlocked Recipe, or production inventory. Refresh the Crafting Hab through this context and retry.`, options.craftingHab.address);
242
+ }
243
+ validateCurrencyCache(currencyCacheAccount, game, currencyCache);
244
+ return {
245
+ character,
246
+ characterAccount,
247
+ currencyCache: currencyCache,
248
+ currencyCacheAccount,
249
+ habAccount,
250
+ playerAccount,
251
+ recipeAccount,
252
+ };
253
+ }
254
+ /** Plans one Crafting Process start without signing, simulation, or writes.
255
+ *
256
+ * The planner validates the loaded Recipe and Starbase Player, optional Hab,
257
+ * known ingredient balances, available crew, and exact whole-account state. It
258
+ * returns one inert Plan whose two required signer roles are bound only during
259
+ * execution.
260
+ *
261
+ * @example
262
+ * ```ts
263
+ * declare const ctx: SageContext;
264
+ * declare const recipe: RecipeSnapshot;
265
+ * declare const player: StarbasePlayerSnapshot;
266
+ * declare const options: PlanStartCraftingProcessOptions;
267
+ * const plan = await planStartCraftingProcess(ctx, recipe, player, options);
268
+ * console.log(plan.describe());
269
+ * ```
270
+ */
271
+ export async function planStartCraftingProcess(ctx, recipe, player, input) {
272
+ const options = validateStartOptions(input);
273
+ const game = requireGameAddress(ctx);
274
+ const canonicalRecipe = await deriveRecipeAddress(game, recipe.id);
275
+ if (recipe.game.address !== game || canonicalRecipe !== recipe.address) {
276
+ fail(`Recipe ${recipe.address} does not match the configured Game ${game} or its canonical recipe-id PDA. Refresh the Recipe through this context and retry.`, recipe.address);
277
+ }
278
+ if (player.playerProfile.address !== options.authorization.profile) {
279
+ fail(`Starbase Player ${player.address} does not belong to authorization Profile ${options.authorization.profile}. Refresh the Starbase Player for that Profile and retry.`, player.address);
280
+ }
281
+ const canonicalPlayer = await deriveStarbasePlayerAddress(player.system.address, player.character.address);
282
+ if (canonicalPlayer !== player.address) {
283
+ fail(`Starbase Player ${player.address} is not canonical for its Character and System. Refresh the Starbase Player through this context and retry.`, player.address);
284
+ }
285
+ if (recipe.status !== 'active') {
286
+ fail(`Recipe ${recipe.name} is ${recipe.status}, so it cannot start a Crafting Process. Choose an active Recipe, refresh it, and retry.`, recipe.address);
287
+ }
288
+ if (recipe.usageLimitRaw !== 0n &&
289
+ (recipe.usageCountRaw >= recipe.usageLimitRaw ||
290
+ options.quantity > recipe.usageLimitRaw - recipe.usageCountRaw)) {
291
+ const remaining = recipe.usageCountRaw >= recipe.usageLimitRaw
292
+ ? 0n
293
+ : recipe.usageLimitRaw - recipe.usageCountRaw;
294
+ fail(`Recipe ${recipe.name} usage limit has remaining ${String(remaining)}, but requested ${String(options.quantity)}. Reduce quantity or choose another active Recipe before retrying.`, recipe.address);
295
+ }
296
+ if (options.numCrew > player.availableCrew) {
297
+ fail(`Starbase Player ${player.address} has only ${String(player.availableCrew)} crew available, but numCrew requested ${String(options.numCrew)}. Reduce numCrew or free crew, refresh the Starbase Player, and retry.`, player.address);
298
+ }
299
+ if (recipe.habRequired && options.craftingHab === undefined) {
300
+ fail(`Recipe ${recipe.name} requires a Crafting Hab. Provide craftingHab from the same Profile and Star System before retrying.`, recipe.address);
301
+ }
302
+ if (options.craftingHab !== undefined) {
303
+ if (options.craftingHab.game.address !== game) {
304
+ fail(`Crafting Hab ${options.craftingHab.address} does not match the configured Game ${game}. Refresh the Crafting Hab through this context and retry.`, options.craftingHab.address);
305
+ }
306
+ if (options.craftingHab.ownerProfile.address !== options.authorization.profile) {
307
+ fail(`Crafting Hab ${options.craftingHab.address} does not belong to Profile ${options.authorization.profile}. Choose a Hab owned by the authorization Profile and retry.`, options.craftingHab.address);
308
+ }
309
+ if (options.craftingHab.system.address !== player.system.address) {
310
+ fail(`Crafting Hab ${options.craftingHab.address} is not at Starbase Player System ${player.system.address}. Choose a Hab at the same Starbase and retry.`, options.craftingHab.address);
311
+ }
312
+ if (options.craftingHab.state.kind !== 'active') {
313
+ fail(`Crafting Hab ${options.craftingHab.address} is ${options.craftingHab.state.kind}; it must be active before starting a Crafting Process. Refresh or reactivate the Hab and retry.`, options.craftingHab.address);
314
+ }
315
+ if (options.craftingHab.availableJobSlots < 1) {
316
+ fail(`Crafting Hab ${options.craftingHab.address} has no available job slots. Complete or cancel another Hab job, refresh it, and retry.`, options.craftingHab.address);
317
+ }
318
+ if (!options.craftingHab.unlockedRecipes.some(({ address }) => address === recipe.address)) {
319
+ fail(`Crafting Hab ${options.craftingHab.address} has not unlocked Recipe ${recipe.name}. Choose an unlocked Recipe or update the Hab before retrying.`, options.craftingHab.address);
320
+ }
321
+ validateKnownInputs(recipe, options.craftingHab.production.inventory.items, options.quantity, 'Crafting Hab', options.craftingHab.address);
322
+ }
323
+ else {
324
+ validateKnownInputs(recipe, player.cargo.items, options.quantity, 'Starbase Player', player.address);
325
+ }
326
+ const resolved = await resolveAccounts(ctx, recipe, player, options, game);
327
+ const description = `Start crafting ${String(options.quantity)} units of ${recipe.name} with ${String(options.numCrew)} crew at Starbase Player ${player.address}.`;
328
+ const preconditions = [
329
+ precondition(resolved.characterAccount, `The canonical Character remains assigned to Profile ${options.authorization.profile} in the configured Game.`),
330
+ precondition(resolved.playerAccount, `Starbase Player ${player.address} retains its Character, Profile, System, crew, and input inventory.`),
331
+ precondition(resolved.recipeAccount, `Recipe ${recipe.name} remains active with the validated usage limit and ingredients.`),
332
+ ...(resolved.habAccount === undefined
333
+ ? []
334
+ : [
335
+ precondition(resolved.habAccount, `Crafting Hab ${resolved.habAccount.address} remains active with an available job slot, the Recipe unlocked, and sufficient inputs.`),
336
+ precondition(resolved.currencyCacheAccount, 'The canonical CurrencyCache remains assigned to the configured Game for Hab crafting.'),
337
+ ]),
338
+ ];
339
+ return createPlan({
340
+ kind: 'crafting.start-process',
341
+ preconditions: await Promise.all(preconditions),
342
+ steps: [
343
+ {
344
+ describes: description,
345
+ instruction: adaptStartCraftingProcessInstruction({
346
+ authorization: options.authorization,
347
+ character: resolved.character,
348
+ craftingProcess: options.craftingProcess,
349
+ game,
350
+ numCrew: options.numCrew,
351
+ quantity: options.quantity,
352
+ recipe: recipe.address,
353
+ starbasePlayer: player.address,
354
+ system: player.system.address,
355
+ ...(options.craftingHab === undefined
356
+ ? {}
357
+ : {
358
+ craftingHabInstance: options.craftingHab.address,
359
+ currencyCache: resolved.currencyCache,
360
+ }),
361
+ }),
362
+ signers: [options.authorization.authority, options.craftingProcess],
363
+ },
364
+ ],
365
+ summary: description,
366
+ });
367
+ }
368
+ function sameProcessSnapshot(process, account) {
369
+ return (account.version === process.version &&
370
+ account.recipe === process.recipe.address &&
371
+ account.profile === process.profile.address &&
372
+ account.system === process.system.address &&
373
+ account.starbasePlayer === process.starbasePlayer.address &&
374
+ account.starbaseSequenceId === process.starbaseSequenceId &&
375
+ account.craftingHab === process.craftingHab.address &&
376
+ account.quantityRaw === process.quantityRaw &&
377
+ account.numCrew === process.numCrew &&
378
+ account.startsAtUnixSeconds === process.startsAtUnixSeconds &&
379
+ account.endsAtUnixSeconds === process.endsAtUnixSeconds &&
380
+ account.feeEscrowRaw === process.feeEscrowRaw &&
381
+ account.inputs.sequenceId === process.inputs.sequenceId &&
382
+ account.inputs.storageCostRaw === process.inputs.usedStorageUnitsRaw &&
383
+ sameBalances(process.inputs.items, account.inputs.amounts));
384
+ }
385
+ function validateGameRead(account, game) {
386
+ if (account.address !== game ||
387
+ account.owner !== sage.SAGE_PROGRAM_ADDRESS ||
388
+ account.data.length < sage.GAME_DISCRIMINATOR.length ||
389
+ !sage.GAME_DISCRIMINATOR.every((byte, index) => account.data[index] === byte)) {
390
+ fail(`Game ${account.address} does not match the configured address, owner, or pinned account layout. Refresh the configured Game and retry.`, account.address);
391
+ }
392
+ }
393
+ async function resolveCompleteAccounts(ctx, process, options, game) {
394
+ const character = await findCharacterPda(options.authorization.profile, game);
395
+ const currencyCache = await deriveCurrencyCacheAddress(game);
396
+ const habAddress = process.craftingHab.address === sage.SAGE_PROGRAM_ADDRESS
397
+ ? undefined
398
+ : process.craftingHab.address;
399
+ const addresses = [
400
+ process.address,
401
+ process.recipe.address,
402
+ process.starbasePlayer.address,
403
+ process.system.address,
404
+ ...(habAddress === undefined ? [] : [habAddress]),
405
+ character,
406
+ game,
407
+ currencyCache,
408
+ ];
409
+ const reads = await readContextRawAccounts(ctx, addresses);
410
+ const byAddress = new Map(addresses.map((address, index) => [address, reads[index]]));
411
+ const processAccount = requireAccount(byAddress.get(process.address), process.address, `Crafting Process ${process.address} is missing, so it may already be cancelled or closed. Refresh the Crafting Process and retry only if it still exists.`);
412
+ const recipeAccount = requireAccount(byAddress.get(process.recipe.address), process.recipe.address, `Recipe ${process.recipe.address} is missing. Refresh the Crafting Process and its Recipe before retrying completion.`);
413
+ const playerAccount = requireAccount(byAddress.get(process.starbasePlayer.address), process.starbasePlayer.address, `Starbase Player ${process.starbasePlayer.address} is missing. Refresh the Crafting Process and Starbase Player before retrying completion.`);
414
+ const systemAccount = requireAccount(byAddress.get(process.system.address), process.system.address, `Star System ${process.system.address} is missing. Refresh the Crafting Process and Star System before retrying completion.`);
415
+ const characterAccount = requireAccount(byAddress.get(character), character, `Canonical Character ${character} is missing for Profile ${options.authorization.profile}. Create or repair the Character in the configured Game before retrying completion.`);
416
+ const gameAccount = requireAccount(byAddress.get(game), game, `Configured Game ${game} is missing. Refresh the configured Game before retrying completion.`);
417
+ const currencyCacheAccount = requireAccount(byAddress.get(currencyCache), currencyCache, `Canonical CurrencyCache ${currencyCache} is missing for the configured Game. Refresh the Game currency configuration before retrying completion.`);
418
+ const habAccount = habAddress === undefined
419
+ ? undefined
420
+ : requireAccount(byAddress.get(habAddress), habAddress, `Crafting Hab ${habAddress} is missing. Refresh the Crafting Process and Hab before retrying completion.`);
421
+ const processData = craftingProcessAccountDefinition.decode(processAccount);
422
+ if (processAccount.address !== process.address ||
423
+ !sameProcessSnapshot(process, processData)) {
424
+ fail(`Crafting Process ${process.address} changed or no longer matches its relationships, timing, crew, quantity, fee, or inputs. Refresh the Crafting Process and retry.`, process.address);
425
+ }
426
+ const recipeData = recipeAccountDefinition.decode(recipeAccount);
427
+ const canonicalRecipe = await deriveRecipeAddress(game, recipeData.id);
428
+ if (recipeAccount.address !== process.recipe.address ||
429
+ canonicalRecipe !== process.recipe.address ||
430
+ recipeData.game !== game ||
431
+ recipeData.id !== process.recipe.id ||
432
+ recipeData.name !== process.recipe.name ||
433
+ recipeData.status !== process.recipe.status ||
434
+ recipeData.habRequired !== process.recipe.habRequired ||
435
+ recipeData.usageCountRaw !== process.recipe.usageCountRaw ||
436
+ recipeData.usageLimitRaw !== process.recipe.usageLimitRaw ||
437
+ !sameBalances(process.recipe.ingredients, recipeData.inputs) ||
438
+ !sameBalances(process.recipe.outputs, recipeData.outputs)) {
439
+ fail(`Recipe ${process.recipe.address} changed or does not match the configured Game, canonical recipe-id PDA, status, Hab policy, limits, ingredients, or outputs. Refresh the Crafting Process and Recipe before retrying completion.`, process.recipe.address);
440
+ }
441
+ if (recipeData.habRequired && habAddress === undefined) {
442
+ fail(`Recipe ${recipeData.name} requires a Crafting Hab, but Crafting Process ${process.address} omits one. Refresh or repair the Process relationship before retrying completion.`, process.address);
443
+ }
444
+ const playerData = starbasePlayerAccountDefinition.decode(playerAccount);
445
+ const canonicalPlayer = await deriveStarbasePlayerAddress(process.system.address, character);
446
+ if (playerAccount.address !== process.starbasePlayer.address ||
447
+ canonicalPlayer !== process.starbasePlayer.address ||
448
+ playerData.playerProfile !== options.authorization.profile ||
449
+ playerData.character !== character ||
450
+ playerData.system !== process.system.address) {
451
+ fail(`Starbase Player ${process.starbasePlayer.address} does not match the authorization Profile, canonical Character, or Crafting Process System. Refresh the Crafting Process and Starbase Player before retrying completion.`, process.starbasePlayer.address);
452
+ }
453
+ const systemData = starSystemAccountDefinition.decode(systemAccount);
454
+ const canonicalSystem = await deriveStarSystemAddress(game, systemData.systemId);
455
+ if (systemAccount.address !== process.system.address ||
456
+ canonicalSystem !== process.system.address ||
457
+ systemData.game !== game ||
458
+ systemData.sequenceId !== process.starbaseSequenceId ||
459
+ systemData.starbase === undefined) {
460
+ fail(`Star System ${process.system.address} does not match the configured Game, canonical system-id PDA, active Starbase, or Crafting Process Starbase sequence. Refresh the Crafting Process and Star System before retrying completion.`, process.system.address);
461
+ }
462
+ if (habAccount !== undefined) {
463
+ const habData = craftingHabAccountDefinition.decode(habAccount);
464
+ if (habAccount.address !== habAddress ||
465
+ habData.game !== game ||
466
+ habData.ownerProfile !== options.authorization.profile ||
467
+ habData.system !== process.system.address ||
468
+ habData.systemSequenceId !== process.starbaseSequenceId ||
469
+ habData.state.kind !== 'active') {
470
+ fail(`Crafting Hab ${habAddress} is not active or does not match the configured Game, authorization Profile, Crafting Process System, or Starbase sequence. Refresh the Crafting Process and Hab before retrying completion.`, habAccount.address);
471
+ }
472
+ }
473
+ const characterData = characterAccountDefinition.decode(characterAccount);
474
+ if (characterAccount.address !== character ||
475
+ characterData.playerProfile !== options.authorization.profile ||
476
+ characterData.game !== game) {
477
+ fail(`The canonical Character does not match Profile ${options.authorization.profile} and the configured Game ${game}. Refresh or repair the Character relationship before retrying completion.`, characterAccount.address);
478
+ }
479
+ validateGameRead(gameAccount, game);
480
+ validateCurrencyCache(currencyCacheAccount, game, currencyCache);
481
+ return {
482
+ character,
483
+ characterAccount,
484
+ currencyCache,
485
+ currencyCacheAccount,
486
+ gameAccount,
487
+ ...(habAccount === undefined ? {} : { habAccount }),
488
+ playerAccount,
489
+ processAccount,
490
+ recipeAccount,
491
+ systemAccount,
492
+ };
493
+ }
494
+ /** Plans completion of one Crafting Process without signing, simulation, or writes.
495
+ *
496
+ * The planner resolves and validates the Process account graph, requires Profile
497
+ * ownership, and checks the translated lifecycle at the caller-provided Unix
498
+ * timestamp. Output capacity, permissions, XP, closure, and final execution
499
+ * freshness remain chain-authoritative.
500
+ *
501
+ * @example
502
+ * ```ts
503
+ * declare const ctx: SageContext;
504
+ * declare const process: CraftingProcessSnapshot;
505
+ * declare const options: PlanCompleteCraftingProcessOptions;
506
+ * const plan = await planCompleteCraftingProcess(ctx, process, options);
507
+ * console.log(plan.describe());
508
+ * ```
509
+ */
510
+ export async function planCompleteCraftingProcess(ctx, process, input) {
511
+ const options = validateCompleteOptions(input);
512
+ const game = requireGameAddress(ctx);
513
+ if (process.profile.address !== options.authorization.profile) {
514
+ fail(`Crafting Process ${process.address} does not belong to authorization Profile ${options.authorization.profile}. Use the owning Profile, refresh the Process, and retry.`, process.address);
515
+ }
516
+ const state = deriveInternalCraftingProcessState(process, options.atUnixSeconds);
517
+ if (state.kind !== 'complete') {
518
+ const timing = state.kind === 'scheduled'
519
+ ? 'scheduled and not complete'
520
+ : `in-progress and not complete with ${String(state.remainingSeconds)} seconds remaining`;
521
+ fail(`Crafting Process ${process.address} is ${timing} at Unix timestamp ${String(options.atUnixSeconds)}. Wait until it is complete, provide a current timestamp, and retry.`, process.address);
522
+ }
523
+ const resolved = await resolveCompleteAccounts(ctx, process, options, game);
524
+ const description = `Complete crafting ${String(process.quantityRaw)} units of ${process.recipe.name} at Starbase Player ${process.starbasePlayer.address}, returning ${String(process.numCrew)} crew and producing the Recipe outputs.`;
525
+ const preconditions = [
526
+ precondition(resolved.processAccount, `Crafting Process ${process.address} retains its validated relationships, timing, crew, quantity, fee, and inputs.`),
527
+ precondition(resolved.recipeAccount, `Recipe ${process.recipe.name} retains its validated Game, Hab policy, ingredients, and outputs.`),
528
+ precondition(resolved.playerAccount, `Starbase Player ${process.starbasePlayer.address} retains its Profile, Character, and System relationships.`),
529
+ precondition(resolved.systemAccount, `Star System ${process.system.address} retains its active Starbase and validated sequence.`),
530
+ ...(resolved.habAccount === undefined
531
+ ? []
532
+ : [
533
+ precondition(resolved.habAccount, `Crafting Hab ${resolved.habAccount.address} remains active in the validated Game, Profile, System, and Starbase sequence.`),
534
+ ]),
535
+ precondition(resolved.characterAccount, `The canonical Character remains assigned to Profile ${options.authorization.profile} in the configured Game.`),
536
+ precondition(resolved.gameAccount, `Game ${game} retains its pinned owner and account layout.`),
537
+ precondition(resolved.currencyCacheAccount, 'The canonical CurrencyCache remains assigned to the configured Game.'),
538
+ ];
539
+ return createPlan({
540
+ kind: 'crafting.complete-process',
541
+ preconditions: await Promise.all(preconditions),
542
+ steps: [
543
+ {
544
+ describes: description,
545
+ instruction: adaptCompleteCraftingProcessInstruction({
546
+ authorization: options.authorization,
547
+ character: resolved.character,
548
+ craftingProcess: process.address,
549
+ currencyCache: resolved.currencyCache,
550
+ game,
551
+ recipe: process.recipe.address,
552
+ starbasePlayer: process.starbasePlayer.address,
553
+ system: process.system.address,
554
+ ...(resolved.habAccount === undefined
555
+ ? {}
556
+ : { craftingHabInstance: resolved.habAccount.address }),
557
+ }),
558
+ signers: [options.authorization.authority],
559
+ },
560
+ ],
561
+ summary: description,
562
+ });
563
+ }
564
+ async function resolveCancelAccounts(ctx, process, options, game) {
565
+ const character = await findCharacterPda(options.authorization.profile, game);
566
+ const habAddress = process.craftingHab.address === sage.SAGE_PROGRAM_ADDRESS
567
+ ? undefined
568
+ : process.craftingHab.address;
569
+ const addresses = [
570
+ process.address,
571
+ process.recipe.address,
572
+ process.starbasePlayer.address,
573
+ process.system.address,
574
+ ...(habAddress === undefined ? [] : [habAddress]),
575
+ character,
576
+ game,
577
+ ];
578
+ const reads = await readContextRawAccounts(ctx, addresses);
579
+ const byAddress = new Map(addresses.map((address, index) => [address, reads[index]]));
580
+ const processAccount = requireAccount(byAddress.get(process.address), process.address, `Crafting Process ${process.address} is missing, so it may already be completed, cancelled, or closed. Refresh the Crafting Process and retry only if it still exists.`);
581
+ const systemAccount = requireAccount(byAddress.get(process.system.address), process.system.address, `Star System ${process.system.address} is missing. Refresh the Crafting Process and Star System before retrying cancellation.`);
582
+ const processData = craftingProcessAccountDefinition.decode(processAccount);
583
+ if (processAccount.address !== process.address ||
584
+ !sameProcessSnapshot(process, processData)) {
585
+ fail(`Crafting Process ${process.address} changed or no longer matches its relationships, timing, crew, quantity, fee, or inputs. Refresh the Crafting Process and retry.`, process.address);
586
+ }
587
+ const systemData = starSystemAccountDefinition.decode(systemAccount);
588
+ const canonicalSystem = await deriveStarSystemAddress(game, systemData.systemId);
589
+ if (systemAccount.address !== process.system.address ||
590
+ canonicalSystem !== process.system.address ||
591
+ systemData.game !== game) {
592
+ fail(`Star System ${process.system.address} does not match the configured Game or canonical system-id PDA. Refresh the Crafting Process and Star System before retrying cancellation.`, process.system.address);
593
+ }
594
+ if (systemData.sequenceId !== process.starbaseSequenceId) {
595
+ fail(`The Starbase sequence for Crafting Process ${process.address} is stale at ${String(process.starbaseSequenceId)}, while Star System ${process.system.address} is at sequence ${String(systemData.sequenceId)}. The program requires destroyCraftingProcess for this destroyed-Starbase cleanup case; do not cancel the Process.`, process.address);
596
+ }
597
+ if (systemData.starbase === undefined) {
598
+ fail(`Star System ${process.system.address} does not have an active Starbase. Refresh the Crafting Process and Star System before retrying cancellation.`, process.system.address);
599
+ }
600
+ const recipeAccount = requireAccount(byAddress.get(process.recipe.address), process.recipe.address, `Recipe ${process.recipe.address} is missing. Refresh the Crafting Process and its Recipe before retrying cancellation.`);
601
+ const playerAccount = requireAccount(byAddress.get(process.starbasePlayer.address), process.starbasePlayer.address, `Starbase Player ${process.starbasePlayer.address} is missing. Refresh the Crafting Process and Starbase Player before retrying cancellation.`);
602
+ const characterAccount = requireAccount(byAddress.get(character), character, `Canonical Character ${character} is missing for Profile ${options.authorization.profile}. Create or repair the Character in the configured Game before retrying cancellation.`);
603
+ const gameAccount = requireAccount(byAddress.get(game), game, `Configured Game ${game} is missing. Refresh the configured Game before retrying cancellation.`);
604
+ const habAccount = habAddress === undefined
605
+ ? undefined
606
+ : requireAccount(byAddress.get(habAddress), habAddress, `Crafting Hab ${habAddress} is missing. Refresh the Crafting Process and Hab before retrying cancellation.`);
607
+ const recipeData = recipeAccountDefinition.decode(recipeAccount);
608
+ const canonicalRecipe = await deriveRecipeAddress(game, recipeData.id);
609
+ if (recipeAccount.address !== process.recipe.address ||
610
+ canonicalRecipe !== process.recipe.address ||
611
+ recipeData.game !== game ||
612
+ recipeData.id !== process.recipe.id ||
613
+ recipeData.name !== process.recipe.name ||
614
+ recipeData.status !== process.recipe.status ||
615
+ recipeData.habRequired !== process.recipe.habRequired ||
616
+ recipeData.usageCountRaw !== process.recipe.usageCountRaw ||
617
+ recipeData.usageLimitRaw !== process.recipe.usageLimitRaw ||
618
+ !sameBalances(process.recipe.ingredients, recipeData.inputs) ||
619
+ !sameBalances(process.recipe.outputs, recipeData.outputs)) {
620
+ fail(`Recipe ${process.recipe.address} changed or does not match the configured Game, canonical recipe-id PDA, status, Hab policy, limits, ingredients, or outputs. Refresh the Crafting Process and Recipe before retrying cancellation.`, process.recipe.address);
621
+ }
622
+ if (recipeData.habRequired && habAddress === undefined) {
623
+ fail(`Recipe ${recipeData.name} requires a Crafting Hab, but Crafting Process ${process.address} omits one. Refresh or repair the Process relationship before retrying cancellation.`, process.address);
624
+ }
625
+ const playerData = starbasePlayerAccountDefinition.decode(playerAccount);
626
+ const canonicalPlayer = await deriveStarbasePlayerAddress(process.system.address, character);
627
+ if (playerAccount.address !== process.starbasePlayer.address ||
628
+ canonicalPlayer !== process.starbasePlayer.address ||
629
+ playerData.playerProfile !== options.authorization.profile ||
630
+ playerData.character !== character ||
631
+ playerData.system !== process.system.address) {
632
+ fail(`Starbase Player ${process.starbasePlayer.address} does not match the authorization Profile, canonical Character, or Crafting Process System. Refresh the Crafting Process and Starbase Player before retrying cancellation.`, process.starbasePlayer.address);
633
+ }
634
+ if (habAccount !== undefined) {
635
+ const habData = craftingHabAccountDefinition.decode(habAccount);
636
+ if (habAccount.address !== habAddress ||
637
+ habData.game !== game ||
638
+ habData.ownerProfile !== options.authorization.profile ||
639
+ habData.system !== process.system.address ||
640
+ habData.systemSequenceId !== process.starbaseSequenceId ||
641
+ habData.state.kind !== 'active') {
642
+ fail(`Crafting Hab ${habAddress} is not active or does not match the configured Game, authorization Profile, Crafting Process System, or Starbase sequence. Refresh the Crafting Process and Hab before retrying cancellation.`, habAccount.address);
643
+ }
644
+ }
645
+ const characterData = characterAccountDefinition.decode(characterAccount);
646
+ if (characterAccount.address !== character ||
647
+ characterData.playerProfile !== options.authorization.profile ||
648
+ characterData.game !== game) {
649
+ fail(`The canonical Character does not match Profile ${options.authorization.profile} and the configured Game ${game}. Refresh or repair the Character relationship before retrying cancellation.`, characterAccount.address);
650
+ }
651
+ validateGameRead(gameAccount, game);
652
+ return {
653
+ character,
654
+ characterAccount,
655
+ gameAccount,
656
+ ...(habAccount === undefined ? {} : { habAccount }),
657
+ playerAccount,
658
+ processAccount,
659
+ recipeAccount,
660
+ systemAccount,
661
+ };
662
+ }
663
+ /** Plans cancellation of one unfinished Crafting Process without signing or writes.
664
+ *
665
+ * The planner resolves and validates the Process account graph, requires Profile
666
+ * ownership, and rejects completed or destroyed-Starbase Process snapshots.
667
+ * Final inventory capacity, permissions, returned inputs and crew, and account
668
+ * closure remain chain-authoritative.
669
+ *
670
+ * @example
671
+ * ```ts
672
+ * declare const ctx: SageContext;
673
+ * declare const process: CraftingProcessSnapshot;
674
+ * declare const options: PlanCancelCraftingProcessOptions;
675
+ * const plan = await planCancelCraftingProcess(ctx, process, options);
676
+ * console.log(plan.describe());
677
+ * ```
678
+ */
679
+ export async function planCancelCraftingProcess(ctx, process, input) {
680
+ const options = validateCancelOptions(input);
681
+ const game = requireGameAddress(ctx);
682
+ if (process.profile.address !== options.authorization.profile) {
683
+ fail(`Crafting Process ${process.address} does not belong to authorization Profile ${options.authorization.profile}. Use the owning Profile, refresh the Process, and retry.`, process.address);
684
+ }
685
+ const state = deriveInternalCraftingProcessState(process, options.atUnixSeconds);
686
+ if (state.kind === 'complete') {
687
+ fail(`Crafting Process ${process.address} is complete at Unix timestamp ${String(options.atUnixSeconds)} and cannot be cancelled. Use planCompleteCraftingProcess with a refreshed snapshot instead.`, process.address);
688
+ }
689
+ const resolved = await resolveCancelAccounts(ctx, process, options, game);
690
+ const description = `Cancel crafting ${String(process.quantityRaw)} units of ${process.recipe.name} at Starbase Player ${process.starbasePlayer.address}, returning all inputs and ${String(process.numCrew)} crew and closing the Crafting Process.`;
691
+ const preconditions = [
692
+ precondition(resolved.processAccount, `Crafting Process ${process.address} retains its validated relationships, timing, crew, quantity, fee, and inputs.`),
693
+ precondition(resolved.recipeAccount, `Recipe ${process.recipe.name} retains its validated Game, Hab policy, usage, ingredients, and outputs.`),
694
+ precondition(resolved.playerAccount, `Starbase Player ${process.starbasePlayer.address} retains its Profile, Character, and System relationships.`),
695
+ precondition(resolved.systemAccount, `Star System ${process.system.address} retains its active Starbase and validated sequence.`),
696
+ ...(resolved.habAccount === undefined
697
+ ? []
698
+ : [
699
+ precondition(resolved.habAccount, `Crafting Hab ${resolved.habAccount.address} remains active in the validated Game, Profile, System, and Starbase sequence.`),
700
+ ]),
701
+ precondition(resolved.characterAccount, `The canonical Character remains assigned to Profile ${options.authorization.profile} in the configured Game.`),
702
+ precondition(resolved.gameAccount, `Game ${game} retains its pinned owner and account layout.`),
703
+ ];
704
+ return createPlan({
705
+ kind: 'crafting.cancel-process',
706
+ preconditions: await Promise.all(preconditions),
707
+ steps: [
708
+ {
709
+ describes: description,
710
+ instruction: adaptCancelCraftingProcessInstruction({
711
+ authorization: options.authorization,
712
+ character: resolved.character,
713
+ craftingProcess: process.address,
714
+ game,
715
+ recipe: process.recipe.address,
716
+ starbasePlayer: process.starbasePlayer.address,
717
+ system: process.system.address,
718
+ ...(resolved.habAccount === undefined
719
+ ? {}
720
+ : { craftingHabInstance: resolved.habAccount.address }),
721
+ }),
722
+ signers: [options.authorization.authority],
723
+ },
724
+ ],
725
+ summary: description,
726
+ });
727
+ }
728
+ //# sourceMappingURL=index.js.map