@mindot/will 0.1.0

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 (178) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +777 -0
  3. package/dist/index.d.ts +8144 -0
  4. package/dist/index.js +26231 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +73 -0
  7. package/src/cognition/agency/access.grants.ts +59 -0
  8. package/src/cognition/agency/competence.codec.ts +89 -0
  9. package/src/cognition/agency/engines/action.selector.ts +462 -0
  10. package/src/cognition/agency/engines/affordance.synthesizer.ts +384 -0
  11. package/src/cognition/agency/engines/deliberation.engine.ts +241 -0
  12. package/src/cognition/agency/engines/instruction.intake.ts +54 -0
  13. package/src/cognition/agency/engines/motor.schema.executor.ts +532 -0
  14. package/src/cognition/agency/engines/reafference.engine.ts +241 -0
  15. package/src/cognition/agency/execution.primitives.ts +118 -0
  16. package/src/cognition/agency/index.ts +58 -0
  17. package/src/cognition/agency/proactive.communicator.ts +274 -0
  18. package/src/cognition/agency/reconcile.learning.ts +82 -0
  19. package/src/cognition/agency/schemas/external.ts +59 -0
  20. package/src/cognition/agency/schemas/innate.ts +111 -0
  21. package/src/cognition/agency/schemas/repertoire.ts +246 -0
  22. package/src/cognition/agency/selection.scoring.ts +191 -0
  23. package/src/cognition/agency/types.ts +165 -0
  24. package/src/cognition/bus.ts +391 -0
  25. package/src/cognition/completion.inbox.ts +86 -0
  26. package/src/cognition/config.mirror.entities.ts +475 -0
  27. package/src/cognition/conversation.memory.ts +81 -0
  28. package/src/cognition/event.log.ts +116 -0
  29. package/src/cognition/event.schemas.ts +568 -0
  30. package/src/cognition/faculties/aesthetic.evaluator.ts +313 -0
  31. package/src/cognition/faculties/affective.blender.ts +562 -0
  32. package/src/cognition/faculties/attachment.evaluator.ts +432 -0
  33. package/src/cognition/faculties/attention.allocator.ts +422 -0
  34. package/src/cognition/faculties/autobiographical.narrator.ts +300 -0
  35. package/src/cognition/faculties/bias.detector.ts +363 -0
  36. package/src/cognition/faculties/circadian.oscillator.ts +294 -0
  37. package/src/cognition/faculties/confidence.calibrator.ts +324 -0
  38. package/src/cognition/faculties/dream.simulator.ts +251 -0
  39. package/src/cognition/faculties/empathy.simulator.ts +253 -0
  40. package/src/cognition/faculties/energy.regulator.ts +320 -0
  41. package/src/cognition/faculties/episodic.consolidator.ts +728 -0
  42. package/src/cognition/faculties/executive.engine/commands.ts +459 -0
  43. package/src/cognition/faculties/executive.engine/config.ts +46 -0
  44. package/src/cognition/faculties/executive.engine/context.ts +605 -0
  45. package/src/cognition/faculties/executive.engine/deferred.effects.ts +104 -0
  46. package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +60 -0
  47. package/src/cognition/faculties/executive.engine/effort.gate.ts +114 -0
  48. package/src/cognition/faculties/executive.engine/engine.ts +1028 -0
  49. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +90 -0
  50. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +271 -0
  51. package/src/cognition/faculties/executive.engine/facet.ts +628 -0
  52. package/src/cognition/faculties/executive.engine/gating.ts +222 -0
  53. package/src/cognition/faculties/executive.engine/index.ts +6 -0
  54. package/src/cognition/faculties/executive.engine/messages.ts +102 -0
  55. package/src/cognition/faculties/executive.engine/parser.ts +380 -0
  56. package/src/cognition/faculties/executive.engine/prompt.factory.ts +1053 -0
  57. package/src/cognition/faculties/executive.engine/types.ts +323 -0
  58. package/src/cognition/faculties/exteroception.ts +338 -0
  59. package/src/cognition/faculties/forgetting.curve.ts +202 -0
  60. package/src/cognition/faculties/frustration.evaluator.ts +280 -0
  61. package/src/cognition/faculties/goal.manager.ts +1008 -0
  62. package/src/cognition/faculties/inhibition.controller.ts +351 -0
  63. package/src/cognition/faculties/interoception.ts +417 -0
  64. package/src/cognition/faculties/introspection.engine.ts +246 -0
  65. package/src/cognition/faculties/known.entity.tracker.ts +439 -0
  66. package/src/cognition/faculties/loss.evaluator.ts +224 -0
  67. package/src/cognition/faculties/moral.evaluator.ts +437 -0
  68. package/src/cognition/faculties/novelty.detector.ts +254 -0
  69. package/src/cognition/faculties/persona.consolidator.ts +845 -0
  70. package/src/cognition/faculties/planning.engine/engine.ts +859 -0
  71. package/src/cognition/faculties/planning.engine/plan.frontier.ts +83 -0
  72. package/src/cognition/faculties/planning.engine/plan.store.ts +174 -0
  73. package/src/cognition/faculties/planning.engine/plan.supervision.ts +494 -0
  74. package/src/cognition/faculties/planning.engine/types.ts +143 -0
  75. package/src/cognition/faculties/reputation.tracker.ts +294 -0
  76. package/src/cognition/faculties/reward.evaluator.ts +304 -0
  77. package/src/cognition/faculties/self.model.updater.ts +677 -0
  78. package/src/cognition/faculties/semantic.engine/clustering.ts +673 -0
  79. package/src/cognition/faculties/semantic.engine/index.ts +6 -0
  80. package/src/cognition/faculties/semantic.engine/integrator.ts +750 -0
  81. package/src/cognition/faculties/semantic.engine/types.ts +53 -0
  82. package/src/cognition/faculties/sleep.pressure.regulator.ts +262 -0
  83. package/src/cognition/faculties/social.perception.ts +333 -0
  84. package/src/cognition/faculties/spaced.repetition.ts +605 -0
  85. package/src/cognition/faculties/stress.regulator.ts +375 -0
  86. package/src/cognition/faculties/task.switcher.ts +239 -0
  87. package/src/cognition/faculties/theory.of.mind.ts +333 -0
  88. package/src/cognition/faculties/threat.evaluator.ts +312 -0
  89. package/src/cognition/faculties/working.memory.ts +406 -0
  90. package/src/cognition/generative.model.ts +344 -0
  91. package/src/cognition/heartbeat.ts +70 -0
  92. package/src/cognition/index.ts +291 -0
  93. package/src/cognition/instruction.handler.ts +146 -0
  94. package/src/cognition/memory/index.ts +35 -0
  95. package/src/cognition/memory/vector.adapter.ts +316 -0
  96. package/src/cognition/memory/vector.content.ts +47 -0
  97. package/src/cognition/memory/vector.embedder.ts +212 -0
  98. package/src/cognition/memory/vector.index.ts +459 -0
  99. package/src/cognition/memory/vector.types.ts +65 -0
  100. package/src/cognition/orchestrator.ts +142 -0
  101. package/src/cognition/persona.prior.ts +265 -0
  102. package/src/cognition/schema.registry.ts +145 -0
  103. package/src/cognition/senses/audition.engine/engine.ts +976 -0
  104. package/src/cognition/senses/audition.engine/salience.ts +59 -0
  105. package/src/cognition/senses/base.sense.engine.ts +117 -0
  106. package/src/cognition/senses/gustation.engine.ts +24 -0
  107. package/src/cognition/senses/index.ts +172 -0
  108. package/src/cognition/senses/olfaction.engine.ts +24 -0
  109. package/src/cognition/senses/somatosensation.engine.ts +23 -0
  110. package/src/cognition/senses/vision.engine.ts +23 -0
  111. package/src/cognition/types.ts +63 -0
  112. package/src/cognition/utilities/token.tracker.ts +492 -0
  113. package/src/core/abstracts.ts +60 -0
  114. package/src/core/async.engine.ts +539 -0
  115. package/src/core/clock.ts +196 -0
  116. package/src/core/completion.recorder.ts +122 -0
  117. package/src/core/conflict.detector.ts +140 -0
  118. package/src/core/distributed.ts +626 -0
  119. package/src/core/event.bus.ts +292 -0
  120. package/src/core/inbound.recorder.ts +100 -0
  121. package/src/core/index.ts +167 -0
  122. package/src/core/logger.ts +85 -0
  123. package/src/core/metrics.ts +107 -0
  124. package/src/core/orchestrator.ts +625 -0
  125. package/src/core/replay.ts +589 -0
  126. package/src/core/scenario.ts +120 -0
  127. package/src/core/serialization.ts +448 -0
  128. package/src/core/simulation.ts +197 -0
  129. package/src/core/snapshot.manager.ts +293 -0
  130. package/src/core/state.manager.ts +283 -0
  131. package/src/core/types.ts +274 -0
  132. package/src/core/utils.ts +82 -0
  133. package/src/core/wall.clock.ts +23 -0
  134. package/src/extensions/livestream.ext.ts +570 -0
  135. package/src/extensions/time.ext.ts +339 -0
  136. package/src/index.ts +91 -0
  137. package/src/llm/gate.ts +140 -0
  138. package/src/llm/index.ts +716 -0
  139. package/src/llm/summarizer.ts +170 -0
  140. package/src/llm/wire.contracts.ts +67 -0
  141. package/src/pma/eval.ts +651 -0
  142. package/src/pma/index.ts +1082 -0
  143. package/src/profiles/built-in.ts +7 -0
  144. package/src/profiles/companion.ts +28 -0
  145. package/src/profiles/company-brain.ts +54 -0
  146. package/src/profiles/customer-service.ts +30 -0
  147. package/src/profiles/game-npc.ts +20 -0
  148. package/src/profiles/index.ts +42 -0
  149. package/src/profiles/smart-home.ts +34 -0
  150. package/src/runners/coherence.runner.ts +49 -0
  151. package/src/runners/outreach.runner.ts +158 -0
  152. package/src/runners/social.runner.ts +182 -0
  153. package/src/runners/thin-shim.runner.ts +195 -0
  154. package/src/sdk/will.ts +334 -0
  155. package/src/stem/assembly.audit.ts +88 -0
  156. package/src/stem/distribution.ts +203 -0
  157. package/src/stem/guards/identity.coherence.ts +152 -0
  158. package/src/stem/guards/identity.guard.ts +227 -0
  159. package/src/stem/index.ts +1145 -0
  160. package/src/stem/mind.ts +1090 -0
  161. package/src/stem/tracts/ack.reconciler.ts +55 -0
  162. package/src/stem/tracts/biography.writer.ts +190 -0
  163. package/src/stem/tracts/effector.controller.ts +143 -0
  164. package/src/stem/tracts/health.reporter.ts +113 -0
  165. package/src/stem/tracts/inbound.queue.ts +54 -0
  166. package/src/stem/tracts/outbox.controller.ts +167 -0
  167. package/src/stem/tracts/outbox.writer.ts +133 -0
  168. package/src/stem/tracts/pma.controller.ts +109 -0
  169. package/src/stem/tracts/replay.controller.ts +177 -0
  170. package/src/stem/tracts/sensory.controller.ts +168 -0
  171. package/src/stem/tracts/session.logger.ts +157 -0
  172. package/src/stem/tracts/transport/index.ts +38 -0
  173. package/src/stem/tracts/transport/loopback.transport.ts +86 -0
  174. package/src/stem/tracts/transport/socketio.transport.ts +200 -0
  175. package/src/stem/tracts/transport/stream.transport.ts +152 -0
  176. package/src/stem/tracts/transport/types.ts +193 -0
  177. package/src/stem/tracts/transport.controller.ts +380 -0
  178. package/src/types.ts +116 -0
@@ -0,0 +1,589 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/core/replay.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ /**
6
+ * Deterministic replay system for debugging and analysis.
7
+ * Records all inputs/events and allows frame-accurate replay.
8
+ */
9
+
10
+ import type { SimulationContext, SimulationEvent, Tick, Timestamp } from '#core/types'
11
+ import { BunStorageAdapter, type StorageAdapter } from '#core/abstracts'
12
+ import { DeltaEncoder } from '#core/serialization'
13
+ import type { DeltaSnapshot } from '#core/serialization'
14
+ import type { LLMCompletionRecord, LLMCompletionSink } from '#core/completion.recorder'
15
+ import type { InboundRecord, InboundSink } from '#core/inbound.recorder'
16
+ import { wallClock } from '#core/wall.clock'
17
+
18
+ export interface ReplayRecord {
19
+ tick: Tick
20
+ timestamp: Timestamp
21
+ events: SimulationEvent[]
22
+ delta?: DeltaSnapshot
23
+ }
24
+
25
+ export interface ReplayMetadata {
26
+ simulationId: string
27
+ runId: string
28
+ startTime: Timestamp
29
+ endTime: Timestamp
30
+ totalTicks: Tick
31
+ totalEvents: number
32
+ totalCompletions: number
33
+ /** Count of recorded external inbound envelopes (messages/percepts/acks). */
34
+ totalInbound?: number
35
+ randomSeed: number
36
+ version: string
37
+ tags: Record<string, string>
38
+ }
39
+
40
+ export interface ReplaySession {
41
+ readonly metadata: ReplayMetadata
42
+ readonly currentTick: Tick
43
+ readonly isPlaying: boolean
44
+ readonly isPaused: boolean
45
+ readonly speed: number
46
+ play(): void
47
+ pause(): void
48
+ stop(): void
49
+ seekToTick( tick: Tick ): Promise<void>
50
+ seekToTime( time: Timestamp ): Promise<void>
51
+ stepForward( count?: number ): Promise<SimulationEvent[]>
52
+ stepBackward( count?: number ): Promise<SimulationEvent[]>
53
+ setSpeed( speed: number ): void
54
+ export(): Promise<Uint8Array>
55
+ }
56
+
57
+ export interface ReplayRecorder extends LLMCompletionSink, InboundSink {
58
+ recordEvent( event: SimulationEvent, context: SimulationContext ): void
59
+ recordTick( tick: Tick, events: SimulationEvent[] ): void
60
+ recordCompletion( record: LLMCompletionRecord ): void
61
+ recordInbound( record: InboundRecord ): void
62
+ flush(): Promise<void>
63
+ save( path: string ): Promise<void>
64
+ getMetadata(): ReplayMetadata
65
+ close(): void
66
+ }
67
+
68
+ /** On-disk shape of a flush segment: a batch of tick records + completions. */
69
+ interface ReplaySegment {
70
+ records: ReplayRecord[]
71
+ completions: LLMCompletionRecord[]
72
+ inbound?: InboundRecord[]
73
+ }
74
+
75
+ export interface ReplayConfig {
76
+ bufferSize?: number
77
+ flushIntervalMs?: number
78
+ compress?: boolean
79
+ recordDeltas?: boolean
80
+ maxEventsPerTick?: number
81
+ /**
82
+ * Base path for incremental flush segments. When set, flush() persists the
83
+ * buffered records to `${flushBasePath}.partN.json` and clears the buffer;
84
+ * save() consolidates the segments back into the final file. When unset,
85
+ * flush() retains records in memory (still no data loss) until save().
86
+ */
87
+ flushBasePath?: string
88
+ }
89
+
90
+ // ── Recorder ─────────────────────────────────────────────────
91
+
92
+ export class DefaultReplayRecorder implements ReplayRecorder {
93
+ private _records: ReplayRecord[] = []
94
+ private _completions: LLMCompletionRecord[] = []
95
+ private _inbound: InboundRecord[] = []
96
+ private _metadata: ReplayMetadata
97
+ private _config: ReplayConfig
98
+ private _bufferSize: number
99
+ private _flushInterval: NodeJS.Timeout | null = null
100
+ private _deltaEncoder: DeltaEncoder
101
+ private _storage: StorageAdapter
102
+ private _flushBasePath: string | undefined
103
+ private _segments: string[] = []
104
+ private _segmentCount = 0
105
+
106
+ constructor(
107
+ simulationId: string,
108
+ runId: string,
109
+ randomSeed: number = wallClock(),
110
+ config: ReplayConfig = {},
111
+ storage: StorageAdapter = new BunStorageAdapter()
112
+ ){
113
+ this._config = config
114
+ this._bufferSize = config.bufferSize ?? 1000
115
+ this._deltaEncoder = new DeltaEncoder()
116
+ this._storage = storage
117
+ this._flushBasePath = config.flushBasePath
118
+
119
+ this._metadata = {
120
+ simulationId,
121
+ runId,
122
+ startTime: wallClock(),
123
+ endTime: 0,
124
+ totalTicks: 0,
125
+ totalEvents: 0,
126
+ totalCompletions: 0,
127
+ totalInbound: 0,
128
+ randomSeed,
129
+ version: '1.0.0',
130
+ tags: {}
131
+ }
132
+
133
+ if( config.flushIntervalMs && config.flushIntervalMs > 0 )
134
+ this._flushInterval = setInterval( () => this.flush(), config.flushIntervalMs )
135
+ }
136
+
137
+ recordEvent( event: SimulationEvent, _context: SimulationContext ): void {
138
+ let record = this._records.find( r => r.tick === event.tick )
139
+ if( !record ){
140
+ record = { tick: event.tick, timestamp: event.timestamp, events: [] }
141
+ this._records.push( record )
142
+ }
143
+
144
+ record.events.push( event )
145
+ this._metadata.totalEvents++
146
+
147
+ this._records.length >= this._bufferSize && this.flush().catch( () => {} )
148
+ }
149
+
150
+ recordTick( tick: Tick, events: SimulationEvent[] ): void {
151
+ this._records.push({ tick, timestamp: wallClock(), events: [ ...events ] })
152
+ this._metadata.totalEvents += events.length
153
+ this._metadata.totalTicks = tick
154
+
155
+ this._records.length >= this._bufferSize && this.flush().catch( () => {} )
156
+ }
157
+
158
+ /**
159
+ * Record an LLM completion (input + output + model/params) into the replay
160
+ * stream. The LLM is the system's non-deterministic oracle; capturing each
161
+ * completion is the prerequisite for deterministic re-execution (REORIENT R2,
162
+ * deferred) and is the gap FN3 calls out. Completions ride along with tick
163
+ * records through the same flush/segment/save pipeline.
164
+ */
165
+ recordCompletion( record: LLMCompletionRecord ): void {
166
+ this._completions.push( record )
167
+ this._metadata.totalCompletions++
168
+
169
+ this._records.length >= this._bufferSize && this.flush().catch( () => {} )
170
+ }
171
+
172
+ /**
173
+ * Record an external inbound envelope (message/percept/ack) at the tick it was
174
+ * applied. The transport is a non-deterministic input — capturing each envelope
175
+ * is the prerequisite for re-injecting them on a deterministic re-execution
176
+ * replay. Rides the same flush/segment/save pipeline as completions.
177
+ */
178
+ recordInbound( record: InboundRecord ): void {
179
+ this._inbound.push( record )
180
+ this._metadata.totalInbound = ( this._metadata.totalInbound ?? 0 ) + 1
181
+
182
+ this._records.length >= this._bufferSize && this.flush().catch( () => {} )
183
+ }
184
+
185
+ async flush(): Promise<void> {
186
+ if( this._records.length === 0 && this._completions.length === 0 ) return
187
+
188
+ // Without a persistence target, retain records in memory so they survive
189
+ // until save() — never discard (the previous behaviour silently lost all
190
+ // history once the buffer rolled over at bufferSize).
191
+ if( !this._flushBasePath ) return
192
+
193
+ // Swap the buffers out and reserve the segment name synchronously before
194
+ // any await: recordTick fires flush() without awaiting and keeps pushing,
195
+ // so concurrent flushes must each get a fresh array and a unique segment
196
+ // name (reading _segmentCount after the await would collide and overwrite).
197
+ const batch: ReplaySegment = { records: this._records, completions: this._completions, inbound: this._inbound }
198
+ this._records = []
199
+ this._completions = []
200
+ this._inbound = []
201
+ const segPath = `${this._flushBasePath}.part${this._segmentCount}.json`
202
+ this._segmentCount++
203
+ this._segments.push( segPath )
204
+
205
+ try {
206
+ await this._storage.write( segPath, JSON.stringify( batch ) )
207
+ }
208
+ catch( err ){
209
+ // Persisting failed — drop the reservation and fold the batch back in
210
+ // (ahead of newer records) so save() still captures it from memory.
211
+ this._segments = this._segments.filter( s => s !== segPath )
212
+ this._records = batch.records.concat( this._records )
213
+ this._completions = batch.completions.concat( this._completions )
214
+ this._inbound = ( batch.inbound ?? [] ).concat( this._inbound )
215
+ throw err
216
+ }
217
+ }
218
+
219
+ async save( path: string ): Promise<void> {
220
+ // Consolidate any flushed segments with the live buffers so nothing is lost
221
+ // across buffer rollovers.
222
+ const records: ReplayRecord[] = []
223
+ const completions: LLMCompletionRecord[] = []
224
+ const inbound: InboundRecord[] = []
225
+ for( const seg of this._segments ){
226
+ try {
227
+ const parsed = JSON.parse( await this._storage.read( seg ) ) as ReplaySegment
228
+ records.push( ...( parsed.records ?? [] ) )
229
+ completions.push( ...( parsed.completions ?? [] ) )
230
+ inbound.push( ...( parsed.inbound ?? [] ) )
231
+ } catch { /* segment unreadable — skip rather than abort the save */ }
232
+ }
233
+ records.push( ...this._records )
234
+ completions.push( ...this._completions )
235
+ inbound.push( ...this._inbound )
236
+
237
+ const output = {
238
+ metadata: { ...this._metadata, endTime: wallClock() },
239
+ config: this._config,
240
+ records,
241
+ completions,
242
+ inbound,
243
+ }
244
+ await this._storage.write( path, JSON.stringify( output, null, 2 ) )
245
+
246
+ // Clean up consolidated segment files.
247
+ const del = this._storage.delete
248
+ if( del )
249
+ for( const seg of this._segments )
250
+ await del.call( this._storage, seg ).catch( () => {} )
251
+
252
+ this._segments = []
253
+ this._segmentCount = 0
254
+ this._records = []
255
+ this._completions = []
256
+ this._inbound = []
257
+ }
258
+
259
+ getMetadata(): ReplayMetadata {
260
+ return { ...this._metadata }
261
+ }
262
+
263
+ close(): void {
264
+ this._flushInterval && clearInterval( this._flushInterval )
265
+ this._metadata.endTime = wallClock()
266
+ this.flush().catch( () => {} )
267
+ }
268
+ }
269
+
270
+ // ── Session ──────────────────────────────────────────────────
271
+
272
+ export class DefaultReplaySession implements ReplaySession {
273
+ private _records: ReplayRecord[]
274
+ private _metadata: ReplayMetadata
275
+ private _currentIndex = 0
276
+ private _isPlaying = false
277
+ private _isPaused = false
278
+ private _speed = 1.0
279
+ private _playInterval: NodeJS.Timeout | null = null
280
+ private _eventHandlers: Array<( event: SimulationEvent ) => void> = []
281
+
282
+ constructor( records: ReplayRecord[], metadata: ReplayMetadata ){
283
+ this._records = records.sort( ( a, b ) => a.tick - b.tick )
284
+ this._metadata = metadata
285
+ }
286
+
287
+ get metadata(): ReplayMetadata { return { ...this._metadata } }
288
+ get currentTick(): Tick { return this._records[this._currentIndex]?.tick ?? 0 }
289
+ get isPlaying(): boolean { return this._isPlaying }
290
+ get isPaused(): boolean { return this._isPaused }
291
+ get speed(): number { return this._speed }
292
+
293
+ // ── Playback ─────────────────────────────────────────────
294
+
295
+ play(): void {
296
+ if( this._isPlaying ) return
297
+
298
+ this._isPlaying = true
299
+ this._isPaused = false
300
+
301
+ this._playInterval = setInterval( () => {
302
+ if( !this._isPlaying || this._isPaused ) return
303
+ if( this._currentIndex >= this._records.length - 1 ){
304
+ this.stop()
305
+ return
306
+ }
307
+
308
+ this._currentIndex++
309
+ this._emitEventsForCurrentTick()
310
+ }, 16 / this._speed ) // ~60fps base
311
+ }
312
+
313
+ pause(): void {
314
+ this._isPaused = true
315
+ }
316
+
317
+ stop(): void {
318
+ this._isPlaying = false
319
+ this._isPaused = false
320
+ this._playInterval && clearInterval( this._playInterval )
321
+ this._playInterval = null
322
+ }
323
+
324
+ async seekToTick( targetTick: Tick ): Promise<void> {
325
+ const wasPlaying = this._isPlaying
326
+ if( wasPlaying ) this.pause()
327
+
328
+ let index = this._records.findIndex( r => r.tick >= targetTick )
329
+ if( index === -1 ) index = this._records.length - 1
330
+ if( index < 0 ) index = 0
331
+
332
+ this._currentIndex = index
333
+ await this._emitEventsForCurrentTick()
334
+
335
+ wasPlaying && this.play()
336
+ }
337
+
338
+ async seekToTime( targetTime: Timestamp ): Promise<void> {
339
+ const wasPlaying = this._isPlaying
340
+ wasPlaying && this.pause()
341
+
342
+ let index = this._records.findIndex( r => r.timestamp >= targetTime )
343
+ if( index === -1 ) index = this._records.length - 1
344
+ if( index < 0 ) index = 0
345
+
346
+ this._currentIndex = index
347
+ await this._emitEventsForCurrentTick()
348
+
349
+ wasPlaying && this.play()
350
+ }
351
+
352
+ async stepForward( count: number = 1 ): Promise<SimulationEvent[]> {
353
+ const events: SimulationEvent[] = []
354
+
355
+ for( let i = 0; i < count; i++ ){
356
+ if( this._currentIndex >= this._records.length - 1 ) break
357
+
358
+ this._currentIndex++
359
+ const tickEvents = await this._emitEventsForCurrentTick()
360
+ events.push( ...tickEvents )
361
+ }
362
+
363
+ return events
364
+ }
365
+
366
+ async stepBackward( count: number = 1 ): Promise<SimulationEvent[]> {
367
+ const events: SimulationEvent[] = []
368
+
369
+ for( let i = 0; i < count; i++ ){
370
+ if( this._currentIndex <= 0 ) break
371
+
372
+ this._currentIndex--
373
+ const tickEvents = await this._emitEventsForCurrentTick()
374
+ events.push( ...tickEvents )
375
+ }
376
+
377
+ return events
378
+ }
379
+
380
+ setSpeed( speed: number ): void {
381
+ this._speed = Math.max( 0.1, Math.min( 10, speed ) )
382
+
383
+ if( this._isPlaying && !this._isPaused ){
384
+ this.stop()
385
+ this.play()
386
+ }
387
+ }
388
+
389
+ async export(): Promise<Uint8Array> {
390
+ const encoder = new TextEncoder()
391
+ return encoder.encode( JSON.stringify({ metadata: this._metadata, records: this._records }) )
392
+ }
393
+
394
+ onEvent( handler: ( event: SimulationEvent ) => void ): () => void {
395
+ this._eventHandlers.push( handler )
396
+ return () => {
397
+ const index = this._eventHandlers.indexOf( handler )
398
+ index !== -1 && this._eventHandlers.splice( index, 1 )
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Binary search for a record at a specific tick.
404
+ * O(log n) — records are sorted ascending by tick in the constructor.
405
+ * Used by ReplayManager.compare() to avoid seekToTick side effects
406
+ * and (session as any) type escapes.
407
+ */
408
+ getRecordAtTick( tick: Tick ): ReplayRecord | undefined {
409
+ let lo = 0, hi = this._records.length - 1
410
+
411
+ while( lo <= hi ){
412
+ const mid = ( lo + hi ) >>> 1
413
+ const t = this._records[mid]!.tick
414
+
415
+ if( t === tick ) return this._records[mid]
416
+ t < tick ? lo = mid + 1 : hi = mid - 1
417
+ }
418
+
419
+ return undefined
420
+ }
421
+
422
+ private async _emitEventsForCurrentTick(): Promise<SimulationEvent[]> {
423
+ const record = this._records[this._currentIndex]
424
+ if( !record ) return []
425
+
426
+ for( const handler of this._eventHandlers )
427
+ for( const event of record.events )
428
+ handler( event )
429
+
430
+ return record.events
431
+ }
432
+ }
433
+
434
+ // ── Manager ──────────────────────────────────────────────────
435
+
436
+ export class ReplayManager {
437
+ private _recorders: Map<string, DefaultReplayRecorder> = new Map()
438
+ private _sessions: Map<string, DefaultReplaySession> = new Map()
439
+ private _storage: StorageAdapter
440
+
441
+ constructor( storage: StorageAdapter = new BunStorageAdapter() ){
442
+ this._storage = storage
443
+ }
444
+
445
+ createRecorder(
446
+ simulationId: string,
447
+ runId: string,
448
+ randomSeed?: number,
449
+ config: ReplayConfig = {}
450
+ ): DefaultReplayRecorder {
451
+ const
452
+ key = `${simulationId}:${runId}`,
453
+ recorder = new DefaultReplayRecorder( simulationId, runId, randomSeed, config, this._storage )
454
+
455
+ this._recorders.set( key, recorder )
456
+ return recorder
457
+ }
458
+
459
+ getRecorder( simulationId: string, runId: string ): DefaultReplayRecorder | undefined {
460
+ return this._recorders.get(`${simulationId}:${runId}`)
461
+ }
462
+
463
+ async loadReplay( path: string ): Promise<DefaultReplaySession> {
464
+ if( !( await this._storage.exists( path ) ) )
465
+ throw new Error(`Replay file not found: ${path}`)
466
+
467
+ const content = await this._storage.read( path )
468
+ const parsed = JSON.parse( content )
469
+
470
+ if( !parsed.records || !parsed.metadata )
471
+ throw new Error('Invalid replay file format')
472
+
473
+ const
474
+ session = new DefaultReplaySession( parsed.records, parsed.metadata ),
475
+ key = `${parsed.metadata.simulationId}:${parsed.metadata.runId}`
476
+
477
+ this._sessions.set( key, session )
478
+ return session
479
+ }
480
+
481
+ getSession( simulationId: string, runId: string ): DefaultReplaySession | undefined {
482
+ return this._sessions.get(`${simulationId}:${runId}`)
483
+ }
484
+
485
+ /**
486
+ * Compare two replay files tick-by-tick.
487
+ * Uses getRecordAtTick() for O(log n) lookup per tick — no seek side
488
+ * effects, no type escapes into private session fields.
489
+ */
490
+ async compare( replay1Path: string, replay2Path: string ): Promise<ReplayComparison> {
491
+ const
492
+ session1 = await this.loadReplay( replay1Path ),
493
+ session2 = await this.loadReplay( replay2Path ),
494
+ differences: ReplayDifference[] = [],
495
+ maxTicks = Math.max( session1.metadata.totalTicks, session2.metadata.totalTicks )
496
+
497
+ for( let tick = 0; tick <= maxTicks; tick++ ){
498
+ const
499
+ events1 = session1.getRecordAtTick( tick )?.events ?? [],
500
+ events2 = session2.getRecordAtTick( tick )?.events ?? []
501
+
502
+ // Compare event counts
503
+ if( events1.length !== events2.length )
504
+ differences.push({
505
+ tick,
506
+ type: 'event-mismatch',
507
+ expected: events1.length,
508
+ actual: events2.length,
509
+ path: `tick-${tick}.events.length`,
510
+ })
511
+
512
+ // Compare event types present at this tick
513
+ const
514
+ types1 = new Set( events1.map( e => e.type ) ),
515
+ types2 = new Set( events2.map( e => e.type ) )
516
+
517
+ for( const type of types1 )
518
+ !types2.has( type ) && differences.push({
519
+ tick,
520
+ type: 'event-mismatch',
521
+ expected: type,
522
+ actual: '(missing)',
523
+ path: `tick-${tick}.events.type.${type}`,
524
+ })
525
+
526
+ for( const type of types2 )
527
+ !types1.has( type ) && differences.push({
528
+ tick,
529
+ type: 'event-mismatch',
530
+ expected: '(missing)',
531
+ actual: type,
532
+ path: `tick-${tick}.events.type.${type}`,
533
+ })
534
+
535
+ // Compare individual event payloads in order
536
+ const maxEvents = Math.max( events1.length, events2.length )
537
+ for( let i = 0; i < maxEvents; i++ ){
538
+ const
539
+ e1 = events1[i],
540
+ e2 = events2[i]
541
+
542
+ if( !e1 || !e2 ){
543
+ differences.push({
544
+ tick,
545
+ type: 'event-mismatch',
546
+ expected: e1 ? `event[${i}]: ${e1.type}` : '(missing)',
547
+ actual: e2 ? `event[${i}]: ${e2.type}` : '(missing)',
548
+ path: `tick-${tick}.events[${i}]`,
549
+ })
550
+ continue
551
+ }
552
+
553
+ const
554
+ payload1 = JSON.stringify( e1.payload ),
555
+ payload2 = JSON.stringify( e2.payload )
556
+
557
+ payload1 !== payload2 && differences.push({
558
+ tick,
559
+ type: 'event-mismatch',
560
+ expected: e1.payload,
561
+ actual: e2.payload,
562
+ path: `tick-${tick}.events[${i}].payload`,
563
+ })
564
+ }
565
+ }
566
+
567
+ return {
568
+ areIdentical: differences.length === 0,
569
+ differences,
570
+ totalTicksCompared: maxTicks,
571
+ }
572
+ }
573
+ }
574
+
575
+ // ── Supporting types ─────────────────────────────────────────
576
+
577
+ export interface ReplayComparison {
578
+ areIdentical: boolean
579
+ differences: ReplayDifference[]
580
+ totalTicksCompared: number
581
+ }
582
+
583
+ export interface ReplayDifference {
584
+ tick: Tick
585
+ type: 'entity-mismatch' | 'metric-mismatch' | 'event-mismatch'
586
+ expected: unknown
587
+ actual: unknown
588
+ path?: string
589
+ }
@@ -0,0 +1,120 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/core/scenario.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import type { SimulationState, SimulationContext, SimulationEntity } from '#core/types'
6
+ import type { StateManager } from '#core/state.manager'
7
+
8
+ export interface Scenario {
9
+ readonly id: string
10
+ readonly name: string
11
+ readonly description?: string
12
+ readonly version: string
13
+ readonly tags: string[]
14
+
15
+ /**
16
+ * Populate the live StateManager with initial entities and metrics.
17
+ * Receives the actual StateManager so writes propagate — unlike a
18
+ * snapshot copy which would be silently discarded.
19
+ */
20
+ initialize( state: StateManager, context: SimulationContext ): Promise<void>
21
+
22
+ /**
23
+ * Returns a plain SimulationState snapshot for inspection or seeding
24
+ * before a SimulationContext is available.
25
+ */
26
+ getInitialState(): SimulationState
27
+
28
+ validate(): ScenarioValidationResult
29
+ }
30
+
31
+ export interface ScenarioValidationResult {
32
+ isValid: boolean
33
+ errors: string[]
34
+ warnings: string[]
35
+ }
36
+
37
+ export interface ScenarioConfig {
38
+ id: string
39
+ name: string
40
+ version?: string
41
+ tags?: string[]
42
+ initialEntities?: SimulationEntity[]
43
+ initialMetrics?: Record<string, number>
44
+ parameters?: Record<string, unknown>
45
+ }
46
+
47
+ export class DefaultScenario implements Scenario {
48
+ readonly id: string
49
+ readonly name: string
50
+ readonly description?: string
51
+ readonly version: string
52
+ readonly tags: string[]
53
+ readonly parameters: Record<string, unknown>
54
+
55
+ private _initialEntities: SimulationEntity[]
56
+ private _initialMetrics: Map<string, number>
57
+
58
+ constructor( config: ScenarioConfig ){
59
+ this.id = config.id
60
+ this.name = config.name
61
+ this.version = config.version ?? '1.0.0'
62
+ this.tags = config.tags ?? []
63
+ this.parameters = config.parameters ?? {}
64
+ this._initialEntities = config.initialEntities ?? []
65
+ this._initialMetrics = new Map( Object.entries( config.initialMetrics ?? {} ) )
66
+ }
67
+
68
+ async initialize( state: StateManager, _context: SimulationContext ): Promise<void> {
69
+ state.clear()
70
+
71
+ // Initial entities are stamped from the sim clock, not wall time, so a
72
+ // scenario loaded under a fixed clock yields identical entity timestamps on
73
+ // replay (R2). After clear() the StateManager sits at sim-time 0, and
74
+ // setEntity re-stamps createdAt/updatedAt from that sim time.
75
+ const now = state.currentTime
76
+ for( const entity of this._initialEntities )
77
+ state.setEntity({ ...entity, createdAt: now, updatedAt: now })
78
+
79
+ for( const [ key, value ] of this._initialMetrics )
80
+ state.setMetric( key, value )
81
+
82
+ state.updateClock( 0, now )
83
+ }
84
+
85
+ getInitialState(): SimulationState {
86
+ const entities = new Map<string, SimulationEntity>()
87
+ for( const entity of this._initialEntities )
88
+ entities.set( entity.id, { ...entity })
89
+
90
+ return {
91
+ tick: 0,
92
+ // Deterministic sim-time 0 to match tick 0 — never wall time (R2).
93
+ time: 0,
94
+ entities,
95
+ metrics: new Map( this._initialMetrics ),
96
+ }
97
+ }
98
+
99
+ validate(): ScenarioValidationResult {
100
+ const
101
+ errors: string[] = [],
102
+ warnings: string[] = []
103
+
104
+ if( !this.id ) errors.push('Scenario id is required')
105
+ if( !this.name ) errors.push('Scenario name is required')
106
+
107
+ const ids = new Set<string>()
108
+ for( const entity of this._initialEntities ){
109
+ if( ids.has( entity.id ) ){
110
+ errors.push(`Duplicate entity id: ${entity.id}`)
111
+ continue
112
+ }
113
+
114
+ ids.add( entity.id )
115
+ !entity.type && warnings.push(`Entity ${entity.id} has no type`)
116
+ }
117
+
118
+ return { isValid: errors.length === 0, errors, warnings }
119
+ }
120
+ }