@shipload/sdk 1.0.0-next.7 → 1.0.0-next.71

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 (203) hide show
  1. package/lib/scan.d.ts +52 -0
  2. package/lib/scan.js +162 -0
  3. package/lib/scan.js.map +1 -0
  4. package/lib/scan.m.js +152 -0
  5. package/lib/scan.m.js.map +1 -0
  6. package/lib/shipload.d.ts +5822 -1484
  7. package/lib/shipload.js +20838 -5419
  8. package/lib/shipload.js.map +1 -1
  9. package/lib/shipload.m.js +20674 -5669
  10. package/lib/shipload.m.js.map +1 -1
  11. package/lib/testing.d.ts +1691 -0
  12. package/lib/testing.js +7109 -0
  13. package/lib/testing.js.map +1 -0
  14. package/lib/testing.m.js +7103 -0
  15. package/lib/testing.m.js.map +1 -0
  16. package/package.json +20 -2
  17. package/src/capabilities/craftable.test.ts +82 -0
  18. package/src/capabilities/craftable.ts +82 -0
  19. package/src/capabilities/crafting.test.ts +80 -0
  20. package/src/capabilities/crafting.ts +50 -8
  21. package/src/capabilities/gathering.test.ts +29 -0
  22. package/src/capabilities/gathering.ts +38 -20
  23. package/src/capabilities/hauling.ts +0 -5
  24. package/src/capabilities/index.ts +0 -1
  25. package/src/capabilities/modules.ts +27 -30
  26. package/src/capabilities/movement.ts +1 -1
  27. package/src/capabilities/storage.test.ts +26 -0
  28. package/src/capabilities/storage.ts +22 -7
  29. package/src/contracts/fund.ts +306 -0
  30. package/src/contracts/platform.ts +312 -3
  31. package/src/contracts/server.ts +2793 -478
  32. package/src/contracts/token.ts +170 -0
  33. package/src/contracts.ts +2 -0
  34. package/src/coordinates/address.ts +88 -0
  35. package/src/coordinates/constants.test.ts +15 -0
  36. package/src/coordinates/constants.ts +23 -0
  37. package/src/coordinates/index.ts +15 -0
  38. package/src/coordinates/memo.test.ts +47 -0
  39. package/src/coordinates/memo.ts +20 -0
  40. package/src/coordinates/permutation.ts +77 -0
  41. package/src/coordinates/regions.ts +48 -0
  42. package/src/coordinates/sectors.ts +115 -0
  43. package/src/data/capabilities.ts +49 -18
  44. package/src/data/capability-formulas.ts +26 -14
  45. package/src/data/catalog.test.ts +19 -0
  46. package/src/data/catalog.ts +13 -3
  47. package/src/data/charters.json +396 -0
  48. package/src/data/citizenry-adjectives.json +42 -0
  49. package/src/data/citizenry-nouns.json +50 -0
  50. package/src/data/colors.ts +14 -32
  51. package/src/data/entities.json +421 -15
  52. package/src/data/item-ids.ts +53 -13
  53. package/src/data/items.json +330 -77
  54. package/src/data/kind-registry.json +212 -0
  55. package/src/data/kind-registry.ts +192 -0
  56. package/src/data/metadata.ts +312 -51
  57. package/src/data/recipes-runtime.ts +6 -23
  58. package/src/data/recipes.json +2173 -173
  59. package/src/data/tiers.ts +9 -6
  60. package/src/derivation/build-methods.test.ts +71 -0
  61. package/src/derivation/build-methods.ts +50 -0
  62. package/src/derivation/capabilities.test.ts +348 -0
  63. package/src/derivation/capabilities.ts +707 -0
  64. package/src/derivation/capability-mappings.ts +60 -16
  65. package/src/derivation/crafting.test.ts +17 -0
  66. package/src/derivation/crafting.ts +57 -34
  67. package/src/derivation/index.ts +27 -2
  68. package/src/derivation/recipe-usage.test.ts +88 -0
  69. package/src/derivation/recipe-usage.ts +141 -0
  70. package/src/derivation/reserve-regen.ts +34 -0
  71. package/src/derivation/resources.ts +9 -1
  72. package/src/derivation/rollups.test.ts +55 -0
  73. package/src/derivation/rollups.ts +72 -0
  74. package/src/derivation/stars.test.ts +67 -0
  75. package/src/derivation/stars.ts +25 -0
  76. package/src/derivation/stat-scaling.ts +12 -0
  77. package/src/derivation/stats.ts +6 -6
  78. package/src/derivation/strata.ts +5 -3
  79. package/src/derivation/stratum.ts +26 -22
  80. package/src/derivation/tiers.ts +40 -7
  81. package/src/derivation/upgrades.ts +14 -0
  82. package/src/derivation/wormhole.ts +154 -0
  83. package/src/entities/entity.ts +102 -0
  84. package/src/entities/gamestate.ts +3 -28
  85. package/src/entities/makers.ts +144 -136
  86. package/src/entities/slot-multiplier.ts +43 -0
  87. package/src/entity/hydrate.test.ts +156 -0
  88. package/src/entity/hydrate.ts +86 -0
  89. package/src/errors.ts +21 -16
  90. package/src/format.ts +42 -9
  91. package/src/index-module.ts +425 -32
  92. package/src/influence/ballot.test.ts +103 -0
  93. package/src/influence/ballot.ts +98 -0
  94. package/src/influence/categories.ts +32 -0
  95. package/src/influence/charter-metadata.test.ts +136 -0
  96. package/src/influence/charter-metadata.ts +236 -0
  97. package/src/influence/charters.test.ts +112 -0
  98. package/src/influence/charters.ts +150 -0
  99. package/src/influence/citizenry.ts +38 -0
  100. package/src/influence/civic-shuttle.test.ts +202 -0
  101. package/src/influence/civic-shuttle.ts +142 -0
  102. package/src/influence/civic.test.ts +287 -0
  103. package/src/influence/civic.ts +269 -0
  104. package/src/influence/constants.ts +84 -0
  105. package/src/influence/decay.ts +20 -0
  106. package/src/influence/decomp.ts +95 -0
  107. package/src/influence/demand.ts +106 -0
  108. package/src/influence/duration.ts +101 -0
  109. package/src/influence/index.ts +118 -0
  110. package/src/influence/influence.test.ts +450 -0
  111. package/src/influence/quality.ts +25 -0
  112. package/src/influence/valuation.ts +154 -0
  113. package/src/managers/actions.ts +950 -93
  114. package/src/managers/balances.test.ts +161 -0
  115. package/src/managers/balances.ts +93 -0
  116. package/src/managers/base.ts +10 -2
  117. package/src/managers/cluster.test.ts +39 -0
  118. package/src/managers/cluster.ts +53 -0
  119. package/src/managers/construction-types.ts +80 -0
  120. package/src/managers/construction.ts +453 -0
  121. package/src/managers/context.ts +58 -1
  122. package/src/managers/coordinates.ts +14 -0
  123. package/src/managers/entities.ts +35 -63
  124. package/src/managers/epochs.ts +72 -1
  125. package/src/managers/index.ts +38 -1
  126. package/src/managers/influence.test.ts +56 -0
  127. package/src/managers/influence.ts +425 -0
  128. package/src/managers/jobs.test.ts +96 -0
  129. package/src/managers/jobs.ts +58 -0
  130. package/src/managers/locations.ts +25 -29
  131. package/src/managers/nft.test.ts +21 -0
  132. package/src/managers/nft.ts +110 -0
  133. package/src/managers/players.ts +25 -0
  134. package/src/managers/plot.ts +122 -0
  135. package/src/nft/atomicassets.abi.json +1342 -0
  136. package/src/nft/atomicassets.ts +237 -0
  137. package/src/nft/atomicdata.ts +130 -0
  138. package/src/nft/buildImmutableData.ts +388 -0
  139. package/src/nft/description.ts +222 -100
  140. package/src/nft/index.ts +3 -0
  141. package/src/planner/index.ts +296 -0
  142. package/src/planner/planner.test.ts +459 -0
  143. package/src/receipts.ts +22 -0
  144. package/src/resolution/describe-module.ts +43 -25
  145. package/src/resolution/display-name.ts +39 -11
  146. package/src/resolution/resolve-item.test.ts +48 -0
  147. package/src/resolution/resolve-item.ts +112 -58
  148. package/src/scan/index.ts +244 -0
  149. package/src/scan/scan-wasm.base64.ts +2 -0
  150. package/src/scheduling/accessor.ts +65 -23
  151. package/src/scheduling/availability.test.ts +320 -0
  152. package/src/scheduling/availability.ts +251 -0
  153. package/src/scheduling/cancel.test.ts +550 -0
  154. package/src/scheduling/cancel.ts +254 -0
  155. package/src/scheduling/cluster-stock.test.ts +31 -0
  156. package/src/scheduling/cluster-stock.ts +15 -0
  157. package/src/scheduling/energy.ts +47 -0
  158. package/src/scheduling/epoch.ts +1 -0
  159. package/src/scheduling/idle-resolve.ts +45 -0
  160. package/src/scheduling/jobs.test.ts +448 -0
  161. package/src/scheduling/jobs.ts +343 -0
  162. package/src/scheduling/lane-core.ts +128 -0
  163. package/src/scheduling/lanes.test.ts +250 -0
  164. package/src/scheduling/lanes.ts +279 -0
  165. package/src/scheduling/projection.ts +265 -149
  166. package/src/scheduling/schedule.test.ts +163 -0
  167. package/src/scheduling/schedule.ts +282 -116
  168. package/src/scheduling/task-cargo.test.ts +110 -0
  169. package/src/scheduling/task-cargo.ts +62 -0
  170. package/src/scheduling/task-effects.test.ts +35 -0
  171. package/src/scheduling/task-effects.ts +34 -0
  172. package/src/scheduling/unwrap.test.ts +86 -0
  173. package/src/scheduling/unwrap.ts +190 -0
  174. package/src/shipload.ts +63 -4
  175. package/src/subscriptions/manager.cluster.test.ts +51 -0
  176. package/src/subscriptions/manager.ts +245 -172
  177. package/src/subscriptions/mappers.ts +5 -8
  178. package/src/subscriptions/types.ts +28 -3
  179. package/src/testing/catalog-hash.ts +19 -0
  180. package/src/testing/index.ts +2 -0
  181. package/src/testing/projection-parity.ts +167 -0
  182. package/src/travel/reach.test.ts +86 -0
  183. package/src/travel/reach.ts +34 -0
  184. package/src/travel/route-build.test.ts +63 -0
  185. package/src/travel/route-build.ts +60 -0
  186. package/src/travel/route-planner.ts +288 -0
  187. package/src/travel/route-simulator.ts +170 -0
  188. package/src/travel/travel.ts +190 -123
  189. package/src/types/capabilities.ts +37 -8
  190. package/src/types/entity.ts +3 -3
  191. package/src/types/index.ts +0 -1
  192. package/src/types.ts +81 -23
  193. package/src/utils/cargo.test.ts +14 -0
  194. package/src/utils/cargo.ts +2 -0
  195. package/src/utils/display-name.ts +70 -0
  196. package/src/utils/hash.ts +22 -2
  197. package/src/utils/system.ts +24 -5
  198. package/src/capabilities/loading.ts +0 -8
  199. package/src/entities/container.ts +0 -123
  200. package/src/entities/ship-deploy.ts +0 -295
  201. package/src/entities/ship.ts +0 -221
  202. package/src/entities/warehouse.ts +0 -127
  203. package/src/types/entity-traits.ts +0 -69
@@ -0,0 +1,163 @@
1
+ import {describe, expect, test} from 'bun:test'
2
+ import {ServerContract, TaskType} from '../index-module'
3
+ import {
4
+ appliedTaskCount,
5
+ hasPendingCapper,
6
+ hasResolvable,
7
+ isCapperTaskType,
8
+ orderedTasks,
9
+ unappliedTasks,
10
+ } from './schedule'
11
+
12
+ const T0 = '2026-06-19T00:00:00'
13
+ const NOW = new Date('2026-06-19T00:01:00.000Z')
14
+
15
+ function task(over: Partial<{type: number; duration: number}>) {
16
+ return ServerContract.Types.task.from({
17
+ type: over.type ?? TaskType.TRAVEL,
18
+ duration: over.duration ?? 30,
19
+ cancelable: 0,
20
+ cargo: [],
21
+ couplings: [],
22
+ })
23
+ }
24
+
25
+ function entity(
26
+ tasks: ReturnType<typeof task>[],
27
+ holds: ServerContract.Types.hold[] = [],
28
+ startedISO = T0
29
+ ) {
30
+ return ServerContract.Types.entity_info.from({
31
+ projected_at: 0,
32
+ type: 'ship',
33
+ id: 1,
34
+ owner: 'player.gm',
35
+ entity_name: 'Ship 1',
36
+ coordinates: {x: 0, y: 0, z: 0},
37
+ item_id: 1,
38
+ cargomass: 0,
39
+ cargo: [],
40
+ modules: [],
41
+ lanes: [{lane_key: 0, schedule: {started: startedISO, tasks}}],
42
+ gatherer_lanes: [],
43
+ crafter_lanes: [],
44
+ builder_lanes: [],
45
+ loader_lanes: [],
46
+ holds,
47
+ })
48
+ }
49
+
50
+ function hold(kind: number) {
51
+ return ServerContract.Types.hold.from({
52
+ id: 1,
53
+ kind,
54
+ counterpart: {entity_type: 'ship', entity_id: 2},
55
+ until: T0,
56
+ incoming_mass: 0,
57
+ })
58
+ }
59
+
60
+ describe('isCapperTaskType', () => {
61
+ test('UNDEPLOY, DEMOLISH are cappers', () => {
62
+ expect(isCapperTaskType(TaskType.UNDEPLOY)).toBe(true)
63
+ expect(isCapperTaskType(TaskType.DEMOLISH)).toBe(true)
64
+ })
65
+
66
+ test('REFIT is not a capper', () => {
67
+ expect(isCapperTaskType(TaskType.REFIT)).toBe(false)
68
+ })
69
+
70
+ test('non-capper task types are not cappers', () => {
71
+ expect(isCapperTaskType(TaskType.TRAVEL)).toBe(false)
72
+ expect(isCapperTaskType(TaskType.LOAD)).toBe(false)
73
+ })
74
+ })
75
+
76
+ describe('hasPendingCapper', () => {
77
+ test('false with no schedule', () => {
78
+ expect(hasPendingCapper(entity([]))).toBe(false)
79
+ })
80
+
81
+ test('false with only non-capper tasks queued', () => {
82
+ expect(hasPendingCapper(entity([task({type: TaskType.TRAVEL})]))).toBe(false)
83
+ })
84
+
85
+ test('true when a DEMOLISH task is queued anywhere in the lane', () => {
86
+ const e = entity([task({type: TaskType.TRAVEL}), task({type: TaskType.DEMOLISH})])
87
+ expect(hasPendingCapper(e)).toBe(true)
88
+ })
89
+
90
+ test('true when a DEMOLISH task is queued', () => {
91
+ expect(hasPendingCapper(entity([task({type: TaskType.DEMOLISH})]))).toBe(true)
92
+ })
93
+ })
94
+
95
+ describe('hasResolvable — capper gating', () => {
96
+ test('a completed non-capper front is resolvable', () => {
97
+ const e = entity([task({type: TaskType.TRAVEL, duration: 30})])
98
+ expect(hasResolvable(e, NOW)).toBe(true)
99
+ })
100
+
101
+ test('a completed capper front with no holds is resolvable', () => {
102
+ const e = entity([task({type: TaskType.DEMOLISH, duration: 30})])
103
+ expect(hasResolvable(e, NOW)).toBe(true)
104
+ })
105
+
106
+ test('a completed capper front with a live hold is gated', () => {
107
+ const e = entity([task({type: TaskType.DEMOLISH, duration: 30})], [hold(2)])
108
+ expect(hasResolvable(e, NOW)).toBe(false)
109
+ })
110
+
111
+ test('a capper front clears once holds empty', () => {
112
+ const withHold = entity([task({type: TaskType.DEMOLISH, duration: 30})], [hold(2)])
113
+ const cleared = entity([task({type: TaskType.DEMOLISH, duration: 30})], [])
114
+ expect(hasResolvable(withHold, NOW)).toBe(false)
115
+ expect(hasResolvable(cleared, NOW)).toBe(true)
116
+ })
117
+
118
+ test('a live hold does not gate a non-capper front', () => {
119
+ const e = entity([task({type: TaskType.TRAVEL, duration: 30})], [hold(2)])
120
+ expect(hasResolvable(e, NOW)).toBe(true)
121
+ })
122
+
123
+ test('an in-progress front is not resolvable regardless of holds', () => {
124
+ const e = entity([task({type: TaskType.DEMOLISH, duration: 3600})])
125
+ expect(hasResolvable(e, NOW)).toBe(false)
126
+ })
127
+ })
128
+
129
+ function anchoredEntity(anchorISO: string) {
130
+ const e = entity([
131
+ task({type: TaskType.TRAVEL, duration: 30}),
132
+ task({type: TaskType.TRAVEL, duration: 30}),
133
+ task({type: TaskType.TRAVEL, duration: 30}),
134
+ ])
135
+ return ServerContract.Types.entity_info.from({
136
+ ...ServerContract.Types.entity_info.from(e),
137
+ projected_at: anchorISO,
138
+ })
139
+ }
140
+
141
+ describe('appliedTaskCount / unappliedTasks', () => {
142
+ test('no anchor leaves every task unapplied', () => {
143
+ const e = entity([task({duration: 30}), task({duration: 30})])
144
+ expect(appliedTaskCount({lanes: e.lanes}, orderedTasks(e))).toBe(0)
145
+ expect(unappliedTasks({lanes: e.lanes})).toHaveLength(2)
146
+ })
147
+
148
+ test('tasks completed at or before the anchor are already applied', () => {
149
+ const e = anchoredEntity('2026-06-19T00:01:00')
150
+ expect(appliedTaskCount(e, orderedTasks(e))).toBe(2)
151
+ expect(unappliedTasks(e)).toHaveLength(1)
152
+ })
153
+
154
+ test('the anchor is inclusive, mirroring completed_task_count_at', () => {
155
+ const e = anchoredEntity('2026-06-19T00:00:30')
156
+ expect(appliedTaskCount(e, orderedTasks(e))).toBe(1)
157
+ })
158
+
159
+ test('a whole schedule finished before the anchor leaves nothing unapplied', () => {
160
+ const e = anchoredEntity('2026-06-19T12:00:00')
161
+ expect(unappliedTasks(e)).toHaveLength(0)
162
+ })
163
+ })
@@ -1,193 +1,359 @@
1
+ import type {TimePoint} from '@wharfkit/antelope'
1
2
  import type {ServerContract} from '../contracts'
2
3
  import {TaskType} from '../types'
4
+ import * as core from './lane-core'
3
5
 
4
6
  type Schedule = ServerContract.Types.schedule
5
7
  type Task = ServerContract.Types.task
8
+ type Lane = ServerContract.Types.lane
9
+ type Hold = ServerContract.Types.hold
10
+
11
+ export const LANE_MOBILITY = 0
12
+ export const LANE_BARRIER = 255
6
13
 
7
14
  export interface ScheduleData {
8
- schedule?: Schedule
9
- }
10
-
11
- export interface Scheduleable extends ScheduleData {
12
- hasSchedule: boolean
13
- isIdle: boolean
14
- tasks: Task[]
15
- scheduleDuration(): number
16
- scheduleElapsed(now: Date): number
17
- scheduleRemaining(now: Date): number
18
- scheduleComplete(now: Date): boolean
19
- currentTaskIndex(now: Date): number
20
- currentTask(now: Date): Task | undefined
21
- currentTaskType(now: Date): TaskType | undefined
22
- getTaskStartTime(index: number): number
23
- getTaskElapsed(index: number, now: Date): number
24
- getTaskRemaining(index: number, now: Date): number
25
- isTaskComplete(index: number, now: Date): boolean
26
- isTaskInProgress(index: number, now: Date): boolean
27
- currentTaskProgress(now: Date): number
28
- scheduleProgress(now: Date): number
15
+ lanes?: Lane[]
16
+ holds?: Hold[]
17
+ }
18
+
19
+ export interface AnchoredScheduleData extends ScheduleData {
20
+ projected_at?: TimePoint
21
+ }
22
+
23
+ export interface LaneView {
24
+ laneKey: number
25
+ schedule: Schedule
26
+ }
27
+
28
+ export {
29
+ laneStartsIn,
30
+ currentTaskIndexForLane,
31
+ laneTaskComplete,
32
+ laneTaskInProgress,
33
+ laneCompletesAt,
34
+ currentTaskProgressFloatForLane,
35
+ } from './lane-core'
36
+
37
+ export function getLanes(entity: ScheduleData): LaneView[] {
38
+ const lanes = entity.lanes
39
+ if (!lanes || lanes.length === 0) return []
40
+ return lanes.map((l) => ({laneKey: l.lane_key.toNumber(), schedule: l.schedule}))
41
+ }
42
+
43
+ export function getLane(entity: ScheduleData, laneKey: number): LaneView | undefined {
44
+ const lanes = entity.lanes
45
+ if (!lanes) return undefined
46
+ for (const l of lanes) {
47
+ if (l.lane_key.toNumber() === laneKey) return {laneKey, schedule: l.schedule}
48
+ }
49
+ return undefined
50
+ }
51
+
52
+ export function mobilityLane(entity: ScheduleData): LaneView | undefined {
53
+ return getLane(entity, LANE_MOBILITY)
29
54
  }
30
55
 
31
56
  export function hasSchedule(entity: ScheduleData): boolean {
32
- return !!entity.schedule && entity.schedule.tasks.length > 0
57
+ const lanes = entity.lanes
58
+ if (!lanes) return false
59
+ return lanes.some((l) => l.schedule.tasks.length > 0)
60
+ }
61
+
62
+ export function hasHolds(entity: ScheduleData): boolean {
63
+ const holds = entity.holds
64
+ return !!holds && holds.length > 0
33
65
  }
34
66
 
35
67
  export function isIdle(entity: ScheduleData): boolean {
36
- return !hasSchedule(entity)
68
+ return !hasSchedule(entity) && !hasHolds(entity)
69
+ }
70
+
71
+ // Mirrors is_capper_task_type: demolish/undeploy cap a plan — no further appends once queued.
72
+ export function isCapperTaskType(taskType: number): boolean {
73
+ return taskType === TaskType.UNDEPLOY || taskType === TaskType.DEMOLISH
74
+ }
75
+
76
+ export function hasPendingCapper(entity: ScheduleData): boolean {
77
+ for (const l of entity.lanes ?? []) {
78
+ for (const t of l.schedule.tasks) {
79
+ if (isCapperTaskType(t.type.toNumber())) return true
80
+ }
81
+ }
82
+ return false
83
+ }
84
+
85
+ export function isEntityIdle(entity: ScheduleData, now: Date): boolean {
86
+ if (hasHolds(entity)) return false
87
+ const lanes = entity.lanes
88
+ if (!lanes) return true
89
+ return lanes.every((l) => core.currentTaskIndexForLane(l.schedule, now) < 0)
90
+ }
91
+
92
+ export function entityIdleAt(entity: ScheduleData, _now: Date): Date | undefined {
93
+ const lanes = entity.lanes
94
+ if (!lanes) return undefined
95
+ let maxMs: number | undefined
96
+ for (const l of lanes) {
97
+ if (l.schedule.tasks.length === 0) continue
98
+ const endMs = l.schedule.started.toDate().getTime() + core.laneDuration(l.schedule) * 1000
99
+ if (maxMs === undefined || endMs > maxMs) maxMs = endMs
100
+ }
101
+ return maxMs === undefined ? undefined : new Date(maxMs)
37
102
  }
38
103
 
39
104
  export function getTasks(entity: ScheduleData): Task[] {
40
- return entity.schedule?.tasks || []
105
+ const lanes = entity.lanes
106
+ if (!lanes) return []
107
+ return lanes.flatMap((l) => l.schedule.tasks)
41
108
  }
42
109
 
43
110
  export function scheduleDuration(entity: ScheduleData): number {
44
- if (!entity.schedule) return 0
45
- return entity.schedule.tasks.reduce((sum, task) => sum + task.duration.toNumber(), 0)
111
+ let max = 0
112
+ for (const l of entity.lanes ?? []) max = Math.max(max, core.laneDuration(l.schedule))
113
+ return max
46
114
  }
47
115
 
48
116
  export function scheduleElapsed(entity: ScheduleData, now: Date): number {
49
- if (!entity.schedule) return 0
50
- const started = entity.schedule.started.toDate()
51
- const elapsed = Math.floor((now.getTime() - started.getTime()) / 1000)
52
- return Math.max(0, elapsed)
117
+ let max = 0
118
+ for (const l of entity.lanes ?? []) max = Math.max(max, core.laneElapsed(l.schedule, now))
119
+ return max
53
120
  }
54
121
 
55
122
  export function scheduleRemaining(entity: ScheduleData, now: Date): number {
56
- if (!entity.schedule) return 0
57
- const duration = scheduleDuration(entity)
58
- const elapsed = scheduleElapsed(entity, now)
59
- return Math.max(0, duration - elapsed)
123
+ let remaining = 0
124
+ for (const l of entity.lanes ?? []) {
125
+ remaining = Math.max(remaining, core.laneRemaining(l.schedule, now))
126
+ }
127
+ return remaining
60
128
  }
61
129
 
62
130
  export function scheduleComplete(entity: ScheduleData, now: Date): boolean {
63
- return hasSchedule(entity) && scheduleRemaining(entity, now) === 0
131
+ const lanes = entity.lanes
132
+ if (!lanes) return false
133
+ let hasAnyTask = false
134
+ let remaining = 0
135
+ for (const l of lanes) {
136
+ if (l.schedule.tasks.length > 0) hasAnyTask = true
137
+ remaining = Math.max(remaining, core.laneRemaining(l.schedule, now))
138
+ }
139
+ if (!hasAnyTask) return false
140
+ return remaining === 0
141
+ }
142
+
143
+ // Mirrors lane_front_complete && !capper_front_gated for own-entity holds (workshop/undeploy-target gates need cross-entity context this ScheduleData lacks).
144
+ export function hasResolvable(entity: ScheduleData, now: Date): boolean {
145
+ for (const l of entity.lanes ?? []) {
146
+ if (!core.laneTaskComplete(l.schedule, 0, now)) continue
147
+ const front = l.schedule.tasks[0]
148
+ if (isCapperTaskType(front.type.toNumber()) && hasHolds(entity)) continue
149
+ return true
150
+ }
151
+ return false
64
152
  }
65
153
 
66
- export function currentTaskIndex(entity: ScheduleData, now: Date): number {
67
- if (!entity.schedule || entity.schedule.tasks.length === 0) return -1
154
+ export function currentTaskForLane(
155
+ entity: ScheduleData,
156
+ laneKey: number,
157
+ now: Date
158
+ ): Task | undefined {
159
+ const lane = getLane(entity, laneKey)
160
+ return lane ? core.currentTask(lane.schedule, now) : undefined
161
+ }
68
162
 
69
- const elapsed = scheduleElapsed(entity, now)
70
- let timeAccum = 0
163
+ export function currentTaskTypeForLane(
164
+ entity: ScheduleData,
165
+ laneKey: number,
166
+ now: Date
167
+ ): TaskType | undefined {
168
+ const lane = getLane(entity, laneKey)
169
+ return lane ? core.currentTaskType(lane.schedule, now) : undefined
170
+ }
71
171
 
72
- for (let i = 0; i < entity.schedule.tasks.length; i++) {
73
- const taskDuration = entity.schedule.tasks[i].duration.toNumber()
74
- if (elapsed < timeAccum + taskDuration) {
75
- return i
76
- }
77
- timeAccum += taskDuration
172
+ export function activeTasks(entity: ScheduleData, now: Date): Task[] {
173
+ const out: Task[] = []
174
+ for (const l of entity.lanes ?? []) {
175
+ const idx = core.currentTaskIndexForLane(l.schedule, now)
176
+ if (idx >= 0) out.push(l.schedule.tasks[idx])
78
177
  }
79
-
80
- return -1
178
+ return out
81
179
  }
82
180
 
83
- export function currentTask(entity: ScheduleData, now: Date): Task | undefined {
84
- const index = currentTaskIndex(entity, now)
85
- if (index < 0 || !entity.schedule) return undefined
86
- return entity.schedule.tasks[index]
181
+ export interface ResolvedEvent {
182
+ laneKey: number
183
+ taskIndex: number
184
+ task: Task
185
+ completesAt: Date
87
186
  }
88
187
 
89
- export function currentTaskType(entity: ScheduleData, now: Date): TaskType | undefined {
90
- const task = currentTask(entity, now)
91
- if (!task) return undefined
92
- return task.type.toNumber() as TaskType
188
+ // Canonical lane-front order (mirrors contract front_precedes): completion, then RECHARGE-last, then lane key.
189
+ function frontPrecedes(
190
+ a: {completesAt: Date; task: Task; laneKey: number},
191
+ b: {completesAt: Date; task: Task; laneKey: number}
192
+ ): number {
193
+ if (a.completesAt.getTime() !== b.completesAt.getTime()) {
194
+ return a.completesAt.getTime() - b.completesAt.getTime()
195
+ }
196
+ const aRecharge = a.task.type.toNumber() === TaskType.RECHARGE
197
+ const bRecharge = b.task.type.toNumber() === TaskType.RECHARGE
198
+ if (aRecharge !== bRecharge) return aRecharge ? 1 : -1
199
+ return a.laneKey - b.laneKey
93
200
  }
94
201
 
95
- export function getTaskStartTime(entity: ScheduleData, index: number): number {
96
- if (!entity.schedule || index < 0 || index >= entity.schedule.tasks.length) return 0
97
- let timeAccum = 0
98
- for (let i = 0; i < index; i++) {
99
- timeAccum += entity.schedule.tasks[i].duration.toNumber()
202
+ // Completed lane-fronts in canonical order (mirrors contract front_precedes).
203
+ export function resolveOrder(entity: ScheduleData, now: Date): ResolvedEvent[] {
204
+ const events: ResolvedEvent[] = []
205
+ for (const l of entity.lanes ?? []) {
206
+ const laneKey = l.lane_key.toNumber()
207
+ const startedMs = l.schedule.started.toDate().getTime()
208
+ let endSec = 0
209
+ for (let i = 0; i < l.schedule.tasks.length; i++) {
210
+ const task = l.schedule.tasks[i]
211
+ endSec += task.duration.toNumber()
212
+ const completesAt = new Date(startedMs + endSec * 1000)
213
+ if (completesAt.getTime() > now.getTime()) break
214
+ events.push({laneKey, taskIndex: i, task, completesAt})
215
+ }
100
216
  }
101
- return timeAccum
217
+ events.sort(frontPrecedes)
218
+ return events
102
219
  }
103
220
 
104
- export function getTaskElapsed(entity: ScheduleData, index: number, now: Date): number {
105
- if (!entity.schedule || index < 0 || index >= entity.schedule.tasks.length) return 0
221
+ export interface OrderedTask {
222
+ laneKey: number
223
+ taskIndex: number
224
+ task: Task
225
+ startsAt: Date
226
+ completesAt: Date
227
+ }
106
228
 
107
- const elapsed = scheduleElapsed(entity, now)
108
- const taskStart = getTaskStartTime(entity, index)
109
- const taskDuration = entity.schedule.tasks[index].duration.toNumber()
229
+ // Every task across all lanes in canonical order (mirrors contract front_precedes).
230
+ export function orderedTasks(entity: ScheduleData): OrderedTask[] {
231
+ const out: OrderedTask[] = []
232
+ for (const l of entity.lanes ?? []) {
233
+ const laneKey = l.lane_key.toNumber()
234
+ const startedMs = l.schedule.started.toDate().getTime()
235
+ let endSec = 0
236
+ for (let i = 0; i < l.schedule.tasks.length; i++) {
237
+ const task = l.schedule.tasks[i]
238
+ const startsAt = new Date(startedMs + endSec * 1000)
239
+ endSec += task.duration.toNumber()
240
+ const completesAt = new Date(startedMs + endSec * 1000)
241
+ out.push({laneKey, taskIndex: i, task, startsAt, completesAt})
242
+ }
243
+ }
244
+ out.sort(frontPrecedes)
245
+ return out
246
+ }
110
247
 
111
- if (elapsed <= taskStart) return 0
112
- const elapsedInTask = elapsed - taskStart
113
- return Math.min(elapsedInTask, taskDuration)
248
+ // Mirrors the contract's completed_task_count_at: inclusive at the anchor.
249
+ export function appliedTaskCount(
250
+ entity: AnchoredScheduleData,
251
+ ordered: readonly OrderedTask[]
252
+ ): number {
253
+ const anchor = entity.projected_at
254
+ if (anchor === undefined) return 0
255
+ const anchorMs = Number(anchor.toMilliseconds())
256
+ let count = 0
257
+ for (const {completesAt} of ordered) {
258
+ if (completesAt.getTime() <= anchorMs) count++
259
+ }
260
+ return count
114
261
  }
115
262
 
116
- export function getTaskRemaining(entity: ScheduleData, index: number, now: Date): number {
117
- if (!entity.schedule || index < 0 || index >= entity.schedule.tasks.length) return 0
263
+ // Tasks an entity_info snapshot has NOT yet folded into its cargo and state.
264
+ export function unappliedTasks(entity: AnchoredScheduleData): OrderedTask[] {
265
+ const ordered = orderedTasks(entity)
266
+ return ordered.slice(appliedTaskCount(entity, ordered))
267
+ }
118
268
 
119
- const taskDuration = entity.schedule.tasks[index].duration.toNumber()
120
- const taskElapsed = getTaskElapsed(entity, index, now)
121
- return Math.max(0, taskDuration - taskElapsed)
269
+ export function laneRemainingOf(entity: ScheduleData, laneKey: number, now: Date): number {
270
+ const lane = getLane(entity, laneKey)
271
+ return lane ? core.laneRemaining(lane.schedule, now) : 0
122
272
  }
123
273
 
124
- export function isTaskComplete(entity: ScheduleData, index: number, now: Date): boolean {
125
- if (!entity.schedule || index < 0 || index >= entity.schedule.tasks.length) return false
274
+ export function laneStartsInOf(entity: ScheduleData, laneKey: number, now: Date): number {
275
+ const lane = getLane(entity, laneKey)
276
+ return lane ? core.laneStartsIn(lane.schedule, now) : 0
277
+ }
126
278
 
127
- const taskDuration = entity.schedule.tasks[index].duration.toNumber()
128
- const taskElapsed = getTaskElapsed(entity, index, now)
129
- return taskElapsed >= taskDuration
279
+ export function laneCompleteOf(entity: ScheduleData, laneKey: number, now: Date): boolean {
280
+ const lane = getLane(entity, laneKey)
281
+ return lane ? core.laneComplete(lane.schedule, now) : false
130
282
  }
131
283
 
132
- export function isTaskInProgress(entity: ScheduleData, index: number, now: Date): boolean {
133
- if (!entity.schedule || index < 0 || index >= entity.schedule.tasks.length) return false
284
+ export function laneProgressOf(entity: ScheduleData, laneKey: number, now: Date): number {
285
+ const lane = getLane(entity, laneKey)
286
+ return lane ? core.laneProgress(lane.schedule, now) : 0
287
+ }
288
+
289
+ export function laneTaskElapsedOf(
290
+ entity: ScheduleData,
291
+ laneKey: number,
292
+ index: number,
293
+ now: Date
294
+ ): number {
295
+ const lane = getLane(entity, laneKey)
296
+ return lane ? core.laneTaskElapsed(lane.schedule, index, now) : 0
297
+ }
134
298
 
135
- const taskElapsed = getTaskElapsed(entity, index, now)
136
- const taskDuration = entity.schedule.tasks[index].duration.toNumber()
137
- return taskElapsed > 0 && taskElapsed < taskDuration
299
+ export function laneTaskRemainingOf(
300
+ entity: ScheduleData,
301
+ laneKey: number,
302
+ index: number,
303
+ now: Date
304
+ ): number {
305
+ const lane = getLane(entity, laneKey)
306
+ return lane ? core.laneTaskRemaining(lane.schedule, index, now) : 0
138
307
  }
139
308
 
140
- export function currentTaskProgress(entity: ScheduleData, now: Date): number {
141
- const task = currentTask(entity, now)
142
- if (!task) return 0
143
- const index = currentTaskIndex(entity, now)
144
- const elapsed = getTaskElapsed(entity, index, now)
145
- const duration = task.duration.toNumber()
146
- if (duration === 0) return 1
147
- return Math.min(1, elapsed / duration)
309
+ export function laneTaskCompleteOf(
310
+ entity: ScheduleData,
311
+ laneKey: number,
312
+ index: number,
313
+ now: Date
314
+ ): boolean {
315
+ const lane = getLane(entity, laneKey)
316
+ return lane ? core.laneTaskComplete(lane.schedule, index, now) : false
148
317
  }
149
318
 
150
- export function currentTaskProgressFloat(entity: ScheduleData, now: Date): number {
151
- if (!entity.schedule || entity.schedule.tasks.length === 0) return 0
152
- const index = currentTaskIndex(entity, now)
153
- if (index < 0) return 0
154
- const task = entity.schedule.tasks[index]
155
- const durationMs = task.duration.toNumber() * 1000
156
- if (durationMs === 0) return 1
157
- const startedMs = entity.schedule.started.toDate().getTime()
158
- const taskStartMs = startedMs + getTaskStartTime(entity, index) * 1000
159
- const elapsedMs = now.getTime() - taskStartMs
160
- if (elapsedMs <= 0) return 0
161
- return Math.min(1, elapsedMs / durationMs)
319
+ export function laneTaskInProgressOf(
320
+ entity: ScheduleData,
321
+ laneKey: number,
322
+ index: number,
323
+ now: Date
324
+ ): boolean {
325
+ const lane = getLane(entity, laneKey)
326
+ return lane ? core.laneTaskInProgress(lane.schedule, index, now) : false
162
327
  }
163
328
 
164
- export function scheduleProgress(entity: ScheduleData, now: Date): number {
165
- const duration = scheduleDuration(entity)
166
- if (duration === 0) return hasSchedule(entity) ? 1 : 0
167
- const elapsed = scheduleElapsed(entity, now)
168
- return Math.min(1, elapsed / duration)
329
+ export function currentTaskIndexOf(entity: ScheduleData, laneKey: number, now: Date): number {
330
+ const lane = getLane(entity, laneKey)
331
+ return lane ? core.currentTaskIndexForLane(lane.schedule, now) : -1
169
332
  }
170
333
 
171
- export function isTaskType(entity: ScheduleData, taskType: TaskType, now: Date): boolean {
172
- return currentTaskType(entity, now) === taskType
334
+ function entityDoesTaskType(entity: ScheduleData, taskType: TaskType, now: Date): boolean {
335
+ return activeTasks(entity, now).some((t) => t.type.toNumber() === taskType)
173
336
  }
174
337
 
175
338
  export function isInFlight(entity: ScheduleData, now: Date): boolean {
176
- return isTaskType(entity, TaskType.TRAVEL, now)
339
+ const lane = mobilityLane(entity)
340
+ if (!lane) return false
341
+ const t = core.currentTaskType(lane.schedule, now)
342
+ return t === TaskType.TRAVEL || t === TaskType.TRANSIT
177
343
  }
178
344
 
179
345
  export function isRecharging(entity: ScheduleData, now: Date): boolean {
180
- return isTaskType(entity, TaskType.RECHARGE, now)
346
+ return entityDoesTaskType(entity, TaskType.RECHARGE, now)
181
347
  }
182
348
 
183
349
  export function isLoading(entity: ScheduleData, now: Date): boolean {
184
- return isTaskType(entity, TaskType.LOAD, now)
350
+ return entityDoesTaskType(entity, TaskType.LOAD, now)
185
351
  }
186
352
 
187
353
  export function isUnloading(entity: ScheduleData, now: Date): boolean {
188
- return isTaskType(entity, TaskType.UNLOAD, now)
354
+ return entityDoesTaskType(entity, TaskType.UNLOAD, now)
189
355
  }
190
356
 
191
357
  export function isGathering(entity: ScheduleData, now: Date): boolean {
192
- return isTaskType(entity, TaskType.GATHER, now)
358
+ return entityDoesTaskType(entity, TaskType.GATHER, now)
193
359
  }