@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,293 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/core/snapshot.manager.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ /**
6
+ * SnapshotManager — first-class state serialization for the simulation core.
7
+ *
8
+ * Follows the same pattern as DefaultMetricCollector:
9
+ * - Implements TickMiddleware via an onTick method
10
+ * - Wired into DefaultSimulation via orchestratorConfig.onAfterTick
11
+ * - Always active (configurable interval, not optional existence)
12
+ *
13
+ * Responsibilities:
14
+ * 1. Serialize simulation state at configurable intervals
15
+ * 2. Compute deltas between snapshots
16
+ * 3. Feed the ReplayRecorder with structured tick data
17
+ * 4. Persist snapshots to disk
18
+ * 5. Provide restore points for StateManager.restore()
19
+ */
20
+
21
+ import { logger } from '#core/logger'
22
+ import type { Tick, ReadonlySimulationState, SimulationContext, SimulationState } from '#core/types'
23
+ import type { Serializer, SerializedState, DeltaSnapshot } from '#core/serialization'
24
+ import { BunStorageAdapter, type StorageAdapter } from '#core/abstracts'
25
+ import { DefaultSerializer, DeltaEncoder } from '#core/serialization'
26
+ import { wallClock } from '#core/wall.clock'
27
+
28
+ export interface SnapshotManagerConfig {
29
+ /** How often to take full snapshots (ticks). 1 = every tick. */
30
+ snapshotInterval?: number
31
+ /** How often to persist to disk (ticks). 0 = never. */
32
+ persistInterval?: number
33
+ /** Base path for persisted snapshots */
34
+ persistPath?: string
35
+ /** Whether to compute deltas between snapshots */
36
+ computeDeltas?: boolean
37
+ /** Maximum in-memory snapshots to retain */
38
+ maxInMemorySnapshots?: number
39
+ /** Storage adapter */
40
+ storage?: StorageAdapter
41
+ }
42
+
43
+ export interface SnapshotEntry {
44
+ tick: Tick
45
+ state: SerializedState
46
+ delta?: DeltaSnapshot
47
+ timestamp: number
48
+ }
49
+
50
+ export class SnapshotManager {
51
+ private _serializer: Serializer
52
+ private _deltaEncoder: DeltaEncoder
53
+ private _config: Required<SnapshotManagerConfig>
54
+ private _storage: StorageAdapter
55
+
56
+ private _snapshots: SnapshotEntry[] = []
57
+ /**
58
+ * Deserialized form of the *immediately preceding* snapshot, kept as the
59
+ * baseline for the next delta. Caching the deserialized state (rather than a
60
+ * SerializedState) lets us encode the next delta without re-stringifying and
61
+ * re-deserializing the baseline every snapshot.
62
+ */
63
+ private _prevState?: SimulationState
64
+ private _ticksSinceSnapshot = 0
65
+ private _ticksSincePersist = 0
66
+ private _currentTick: Tick = 0
67
+
68
+ /** Callback for feeding replay recorder — set by DefaultSimulation */
69
+ onSnapshot?: ( tick: Tick, state: SerializedState, delta?: DeltaSnapshot ) => void
70
+
71
+ constructor( config: SnapshotManagerConfig = {} ){
72
+ this._serializer = new DefaultSerializer({ includeChecksum: true, prettyPrint: false })
73
+ this._deltaEncoder = new DeltaEncoder()
74
+ this._storage = config.storage ?? new BunStorageAdapter()
75
+
76
+ this._config = {
77
+ snapshotInterval: config.snapshotInterval ?? 10,
78
+ persistInterval: config.persistInterval ?? 100,
79
+ persistPath: config.persistPath ?? './data/snapshots',
80
+ computeDeltas: config.computeDeltas ?? true,
81
+ maxInMemorySnapshots: config.maxInMemorySnapshots ?? 500,
82
+ storage: this._storage, // satisfies Required<>
83
+ }
84
+
85
+ this._config.persistInterval > 0
86
+ && this._storage.ensureDir?.( this._config.persistPath ).catch(() => {})
87
+ }
88
+
89
+ /**
90
+ * Implements the TickMiddleware signature.
91
+ * Wire this as orchestratorConfig.onAfterTick to enable state serialization.
92
+ * Follows the exact same pattern as DefaultMetricCollector.onTick.
93
+ */
94
+ onTick = ( tick: Tick, state: ReadonlySimulationState, _context: SimulationContext ): void => {
95
+ this._currentTick = tick
96
+ this._ticksSinceSnapshot++
97
+ this._ticksSincePersist++
98
+
99
+ // Take a snapshot at the configured interval
100
+ if( this._ticksSinceSnapshot < this._config.snapshotInterval ) return
101
+
102
+ this._ticksSinceSnapshot = 0
103
+
104
+ // Serialize current state
105
+ const serialized = this._serializer.serialize(
106
+ state as unknown as SimulationState,
107
+ 'json'
108
+ ) as string
109
+
110
+ const parsed = JSON.parse( serialized ) as SerializedState
111
+
112
+ // Compute the delta against the *immediately preceding* snapshot (FN13).
113
+ // The previous code only advanced the baseline when `delta` was falsy
114
+ // (`if( !delta )`), which — after the very first snapshot — pinned the
115
+ // baseline to tick 0 forever: every later delta was computed against that
116
+ // one stale state, growing without bound and, once the tick-0 entry was
117
+ // evicted by the ring buffer, referencing a base that no longer existed.
118
+ // Now each delta describes a single interval of change against the prior
119
+ // snapshot, and the baseline advances every time.
120
+ let delta: DeltaSnapshot | undefined
121
+ let currState: SimulationState | undefined
122
+ if( this._config.computeDeltas ){
123
+ currState = this._serializer.deserialize( serialized )
124
+ if( this._prevState )
125
+ delta = this._deltaEncoder.encode( this._prevState, currState )
126
+ }
127
+
128
+ const entry: SnapshotEntry = {
129
+ tick,
130
+ state: parsed,
131
+ delta,
132
+ timestamp: wallClock(),
133
+ }
134
+
135
+ // Store in memory
136
+ this._snapshots.push( entry )
137
+ if( this._snapshots.length > this._config.maxInMemorySnapshots )
138
+ this._snapshots.shift()
139
+
140
+ // Advance the delta baseline on every snapshot so the next delta is
141
+ // computed against this one. Each stored entry also keeps the full
142
+ // serialized `state`, so it remains a self-contained keyframe for
143
+ // restore() even after its predecessor is evicted.
144
+ if( currState ) this._prevState = currState
145
+
146
+ // Feed external consumers (e.g., ReplayRecorder)
147
+ this.onSnapshot?.( tick, parsed, delta )
148
+
149
+ // Persist to disk
150
+ if( this._config.persistInterval > 0
151
+ && this._ticksSincePersist >= this._config.persistInterval ){
152
+ this._ticksSincePersist = 0
153
+ this._persistSnapshot( entry ).catch( err => {
154
+ logger.error(`[SnapshotManager] Persist failed at tick ${tick}:`, err )
155
+ })
156
+ }
157
+ }
158
+
159
+ // ── Query API ────────────────────────────────────────────
160
+
161
+ /**
162
+ * Retrieve a snapshot by tick.
163
+ */
164
+ getSnapshot( tick: Tick ): SerializedState | undefined {
165
+ return this._snapshots.find( s => s.tick === tick )?.state
166
+ }
167
+
168
+ /**
169
+ * Get all in-memory snapshots.
170
+ */
171
+ getAllSnapshots(): ReadonlyArray<SnapshotEntry> {
172
+ return this._snapshots
173
+ }
174
+
175
+ /**
176
+ * Get the most recent snapshot.
177
+ */
178
+ getLatestSnapshot(): SnapshotEntry | undefined {
179
+ return this._snapshots[ this._snapshots.length - 1 ]
180
+ }
181
+
182
+ /**
183
+ * Restore simulation state from a snapshot.
184
+ * Returns the deserialized state ready for StateManager.restore().
185
+ */
186
+ restoreState( tick: Tick ): SimulationState | undefined {
187
+ const snapshot = this.getSnapshot( tick )
188
+ if( !snapshot ) return undefined
189
+ return this._serializer.deserialize( JSON.stringify( snapshot ) )
190
+ }
191
+
192
+ /**
193
+ * Export all snapshots to a file.
194
+ */
195
+ async exportAll( path: string ): Promise<void> {
196
+ const output = {
197
+ snapshots: this._snapshots.map( s => ({
198
+ tick: s.tick,
199
+ state: s.state,
200
+ delta: s.delta,
201
+ timestamp: s.timestamp,
202
+ })),
203
+ exportedAt: wallClock(),
204
+ }
205
+ await this._storage.write( path, JSON.stringify( output ) )
206
+ }
207
+
208
+ /**
209
+ * Get the number of in-memory snapshots.
210
+ */
211
+ get snapshotCount(): number {
212
+ return this._snapshots.length
213
+ }
214
+
215
+ // ── Restore from storage ──────────────────────────────────
216
+
217
+ /**
218
+ * Load the most recently persisted snapshot from the storage adapter and
219
+ * deserialize it into a live SimulationState, ready for StateManager.restore().
220
+ *
221
+ * Returns undefined when no snapshot has been persisted yet (first boot).
222
+ *
223
+ * Works with any StorageAdapter — BunStorageAdapter (filesystem) or
224
+ * PostgresStorageAdapter (Postgres). The sentinel file `latest.json` is written
225
+ * alongside each snapshot so we can locate the most recent one without scanning.
226
+ */
227
+ async loadLatestFromStorage(): Promise<SimulationState | undefined> {
228
+ if( this._config.persistInterval === 0 ) return undefined
229
+
230
+ const sentinelPath = `${this._config.persistPath}/latest.json`
231
+ try {
232
+ const exists = await this._storage.exists( sentinelPath )
233
+ if( !exists ) return undefined
234
+
235
+ const raw = await this._storage.read( sentinelPath )
236
+ const { path } = JSON.parse( raw ) as { path: string; tick: number }
237
+
238
+ const snapExists = await this._storage.exists( path )
239
+ if( !snapExists ) return undefined
240
+
241
+ const snapRaw = await this._storage.read( path )
242
+ const parsed = JSON.parse( snapRaw ) as SerializedState
243
+ return this._serializer.deserialize( JSON.stringify( parsed ) )
244
+ }
245
+ catch( err ){
246
+ logger.warn( `[SnapshotManager] Could not load latest snapshot:`, err )
247
+ return undefined
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Force an immediate snapshot of `state` and persist it to disk.
253
+ *
254
+ * Called at session end (pauseWill / archiveWill) to guarantee the latest
255
+ * simulation state — including freshly-flushed episodic memories — is
256
+ * written to disk even if the scheduled persist interval has not elapsed.
257
+ *
258
+ * Unlike the normal onTick path, this does NOT push an entry into the
259
+ * in-memory ring buffer (no replay entry needed for a shutdown snapshot)
260
+ * and does NOT advance the delta baseline (`_prevState`; no more ticks will follow).
261
+ *
262
+ * No-op when persistInterval === 0 (disk persistence disabled).
263
+ */
264
+ async persistNow( state: ReadonlySimulationState ): Promise<void> {
265
+ if( this._config.persistInterval === 0 ) return
266
+
267
+ const serialized = this._serializer.serialize(
268
+ state as unknown as SimulationState,
269
+ 'json'
270
+ ) as string
271
+
272
+ const parsed = JSON.parse( serialized ) as SerializedState
273
+ const entry: SnapshotEntry = {
274
+ tick: this._currentTick,
275
+ state: parsed,
276
+ delta: undefined,
277
+ timestamp: wallClock(),
278
+ }
279
+
280
+ await this._persistSnapshot( entry )
281
+ }
282
+
283
+ // ── Internal ─────────────────────────────────────────────
284
+
285
+ private async _persistSnapshot( entry: SnapshotEntry ): Promise<void> {
286
+ const path = `${this._config.persistPath}/snapshot-${String( entry.tick ).padStart( 8, '0')}.json`
287
+ await this._storage.write( path, JSON.stringify( entry.state ) )
288
+
289
+ // Write a sentinel so loadLatestFromStorage() can find this snapshot on restart.
290
+ const sentinelPath = `${this._config.persistPath}/latest.json`
291
+ await this._storage.write( sentinelPath, JSON.stringify({ path, tick: entry.tick }) )
292
+ }
293
+ }
@@ -0,0 +1,283 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/core/state.manager.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import type { SimulationEntity, EntityInput, SimulationState, StateCommands, Tick, Timestamp, RestoreOptions } from '#core/types'
6
+ import { deepFreeze } from '#core/utils'
7
+
8
+ /**
9
+ * Whether entities are deep-frozen on write (R3). Enforcing the read-only
10
+ * contract at runtime costs a recursive freeze per write, so it is on
11
+ * everywhere *except* production — dev, test and CI catch any illegal mutation
12
+ * loudly, and production runs the exact code paths those caught. Override
13
+ * explicitly with `WILL_FREEZE_STATE` (`1`/`true` to force on, `0`/`false` off).
14
+ */
15
+ function resolveFreezeState(): boolean {
16
+ const flag = process.env.WILL_FREEZE_STATE
17
+ if( flag !== undefined && flag !== '' )
18
+ return flag !== '0' && flag.toLowerCase() !== 'false'
19
+ return process.env.NODE_ENV !== 'production'
20
+ }
21
+
22
+ export interface StateManager {
23
+ readonly currentTick: Tick
24
+ readonly currentTime: Timestamp
25
+
26
+ updateClock( tick: Tick, time: Timestamp ): void
27
+
28
+ getEntity<T extends SimulationEntity>( id: string ): T | undefined
29
+ setEntity( entity: EntityInput ): void
30
+ deleteEntity( id: string ): boolean
31
+ getAllEntities(): IterableIterator<SimulationEntity>
32
+
33
+ /** O(1) — backed by an internal type index */
34
+ getEntitiesByType( type: string ): SimulationEntity[]
35
+
36
+ getMetric( key: string ): number | undefined
37
+ setMetric( key: string, value: number ): void
38
+ incrementMetric( key: string, delta?: number ): number
39
+
40
+ /**
41
+ * Apply a batch of commands atomically.
42
+ * Called by the Orchestrator after all engines have returned their
43
+ * EngineResult for a tick (double-buffer commit phase).
44
+ */
45
+ applyCommands( commands: StateCommands ): void
46
+
47
+ snapshot(): SimulationState
48
+ /**
49
+ * Accepts optional RestoreOptions for partial
50
+ * restore. Use { entities: true, metrics: false } to rewind entity
51
+ * state while preserving metric counters.
52
+ */
53
+ restore( snapshot: SimulationState, options?: RestoreOptions ): void
54
+ clear(): void
55
+ }
56
+
57
+ export class DefaultStateManager implements StateManager {
58
+ private _entities = new Map<string, SimulationEntity>()
59
+ private _metrics = new Map<string, number>()
60
+ private _typeIndex = new Map<string, Set<string>>() // type → entity ids
61
+ private _tick: Tick = 0
62
+ private _time: Timestamp = 0
63
+
64
+ /**
65
+ * Copy-on-write guard (R3-b / FIX F8). A handed-out snapshot shares the live
66
+ * `_entities` / `_metrics` map by reference instead of being eagerly copied;
67
+ * these flags mark a map as still observed by an outstanding snapshot. The
68
+ * NEXT mutation clones that map once (see _ownEntities / _ownMetrics) before
69
+ * writing, so the snapshot keeps its point-in-time values. This turns
70
+ * snapshot() into O(1) and collapses the per-tick double copy (pre- + post-
71
+ * commit) into a single clone on the first write — and zero clones on an
72
+ * idle tick or a read-only snapshot consumer. Entities are deep-frozen on
73
+ * write (R3-a), so sharing a reference can never leak a mutable value.
74
+ */
75
+ private _entitiesShared = false
76
+ private _metricsShared = false
77
+
78
+ /**
79
+ * When true, every entity stored is deep-frozen so the read-only snapshot
80
+ * contract (R3) is enforced at runtime, not just in the type system.
81
+ * Resolved once per manager from the environment — see resolveFreezeState.
82
+ */
83
+ private readonly _freeze = resolveFreezeState()
84
+
85
+ get currentTick(): Tick { return this._tick }
86
+ get currentTime(): Timestamp { return this._time }
87
+
88
+ updateClock( tick: Tick, time: Timestamp ): void {
89
+ this._tick = tick
90
+ this._time = time
91
+ }
92
+
93
+ // ── Entity CRUD ──────────────────────────────────────────
94
+
95
+ getEntity<T extends SimulationEntity>( id: string ): T | undefined {
96
+ return this._entities.get( id ) as T | undefined
97
+ }
98
+
99
+ setEntity( entity: EntityInput ): void {
100
+ const
101
+ existing = this._entities.get( entity.id ),
102
+ // Sim clock time (fed each tick by updateClock), NOT wall time — this is the
103
+ // single place every entity's createdAt/updatedAt is stamped, so sourcing it
104
+ // from the sim clock makes all entity timestamps deterministic on replay (R2).
105
+ now = this._time
106
+
107
+ // Maintain type index — handle type changes
108
+ if( existing && existing.type !== entity.type )
109
+ this._typeIndex.get( existing.type )?.delete( entity.id )
110
+
111
+ if( !this._typeIndex.has( entity.type ) )
112
+ this._typeIndex.set( entity.type, new Set() )
113
+
114
+ this._typeIndex.get( entity.type )!.add( entity.id )
115
+
116
+ // A fresh object every write — setEntity never mutates an existing entity
117
+ // in place. Combined with the copy-on-write clone below, the live map only
118
+ // ever changes by slot replacement on a map no snapshot still observes.
119
+ const stored: SimulationEntity = {
120
+ ...entity,
121
+ createdAt: existing?.createdAt ?? entity.createdAt ?? now,
122
+ updatedAt: now,
123
+ updatedAtTick: this._tick,
124
+ }
125
+
126
+ // CoW: clone the map once if a snapshot still references it, so this write
127
+ // can't reach back and mutate that point-in-time view (R3-b / FIX F8).
128
+ this._ownEntities()
129
+
130
+ // Freeze so anything that later reads this entity (engine snapshots, stored
131
+ // rollback snapshots) cannot mutate it and silently corrupt determinism (R3).
132
+ this._entities.set( entity.id, this._freeze ? deepFreeze( stored ) : stored )
133
+ }
134
+
135
+ deleteEntity( id: string ): boolean {
136
+ const existing = this._entities.get( id )
137
+ if( !existing ) return false
138
+
139
+ this._typeIndex.get( existing.type )?.delete( id )
140
+ // CoW: clone before removing the slot so an outstanding snapshot still
141
+ // sees the entity (R3-b / FIX F8).
142
+ this._ownEntities()
143
+ return this._entities.delete( id )
144
+ }
145
+
146
+ getAllEntities(): IterableIterator<SimulationEntity> {
147
+ return this._entities.values()
148
+ }
149
+
150
+ getEntitiesByType( type: string ): SimulationEntity[] {
151
+ const ids = this._typeIndex.get( type )
152
+ if( !ids ) return []
153
+
154
+ const result: SimulationEntity[] = []
155
+ for( const id of ids ){
156
+ const entity = this._entities.get( id )
157
+ entity && result.push( entity )
158
+ }
159
+
160
+ return result
161
+ }
162
+
163
+ // ── Metrics ──────────────────────────────────────────────
164
+
165
+ getMetric( key: string ): number | undefined {
166
+ return this._metrics.get( key )
167
+ }
168
+
169
+ setMetric( key: string, value: number ): void {
170
+ this._ownMetrics()
171
+ this._metrics.set( key, value )
172
+ }
173
+
174
+ incrementMetric( key: string, delta: number = 1 ): number {
175
+ const next = ( this._metrics.get( key ) ?? 0 ) + delta
176
+ this._ownMetrics()
177
+ this._metrics.set( key, next )
178
+ return next
179
+ }
180
+
181
+ // ── Command application (double-buffer commit) ───────────
182
+
183
+ applyCommands( commands: StateCommands ): void {
184
+ if( commands.set )
185
+ for( const entity of commands.set )
186
+ this.setEntity( entity )
187
+
188
+ if( commands.delete )
189
+ for( const id of commands.delete )
190
+ this.deleteEntity( id )
191
+
192
+ if( commands.metrics )
193
+ for( const [ key, value ] of commands.metrics )
194
+ typeof value === 'number'
195
+ ? this.setMetric( key, value )
196
+ : this.incrementMetric( key, ( value as { delta: number } ).delta )
197
+ }
198
+
199
+ // ── Snapshot / restore ───────────────────────────────────
200
+
201
+ snapshot(): SimulationState {
202
+ // Copy-on-write (R3-b / FIX F8): hand out the live maps by reference and
203
+ // mark them shared instead of eagerly cloning. The next mutation clones
204
+ // before writing (_ownEntities / _ownMetrics), so this snapshot stays a
205
+ // genuine point-in-time view. Callers must treat it as read-only — the
206
+ // ReadonlySimulationState type enforces that for the per-tick consumers,
207
+ // and no code mutates a snapshot's maps directly.
208
+ this._entitiesShared = true
209
+ this._metricsShared = true
210
+ return {
211
+ tick: this._tick,
212
+ time: this._time,
213
+ entities: this._entities,
214
+ metrics: this._metrics,
215
+ }
216
+ }
217
+
218
+ /** CoW: clone `_entities` once if an outstanding snapshot still shares it. */
219
+ private _ownEntities(): void {
220
+ if( !this._entitiesShared ) return
221
+ this._entities = new Map( this._entities )
222
+ this._entitiesShared = false
223
+ }
224
+
225
+ /** CoW: clone `_metrics` once if an outstanding snapshot still shares it. */
226
+ private _ownMetrics(): void {
227
+ if( !this._metricsShared ) return
228
+ this._metrics = new Map( this._metrics )
229
+ this._metricsShared = false
230
+ }
231
+
232
+ /**
233
+ * Allows selective rollback of entities, metrics, or clock state.
234
+ * Default behavior (no options) is full restore — backward compatible.
235
+ */
236
+ restore( snapshot: SimulationState, options: RestoreOptions = {} ): void {
237
+ const restoreEntities = options.entities ?? true
238
+ const restoreMetrics = options.metrics ?? true
239
+ const restoreClock = options.clock ?? true
240
+
241
+ if( restoreClock ){
242
+ this._tick = snapshot.tick
243
+ this._time = snapshot.time
244
+ }
245
+
246
+ if( restoreEntities ){
247
+ // Fresh map the manager owns outright — clear any CoW share (R3-b).
248
+ this._entities = new Map( snapshot.entities )
249
+ this._entitiesShared = false
250
+
251
+ // Rebuild type index from restored entities
252
+ this._typeIndex.clear()
253
+ for( const entity of this._entities.values() ){
254
+ // Restored entities may be fresh (e.g. deserialized from disk) and thus
255
+ // unfrozen — re-establish the R3 immutability guarantee. Idempotent for
256
+ // entities that came from an already-frozen in-process snapshot.
257
+ if( this._freeze ) deepFreeze( entity )
258
+
259
+ if( !this._typeIndex.has( entity.type ) )
260
+ this._typeIndex.set( entity.type, new Set() )
261
+
262
+ this._typeIndex.get( entity.type )!.add( entity.id )
263
+ }
264
+ }
265
+
266
+ if( restoreMetrics ){
267
+ this._metrics = new Map( snapshot.metrics )
268
+ this._metricsShared = false
269
+ }
270
+ }
271
+
272
+ clear(): void {
273
+ // Replace (not .clear()) the maps: an outstanding snapshot may still share
274
+ // them under CoW, and emptying them in place would wipe that view (R3-b).
275
+ this._entities = new Map()
276
+ this._metrics = new Map()
277
+ this._entitiesShared = false
278
+ this._metricsShared = false
279
+ this._typeIndex.clear()
280
+ this._tick = 0
281
+ this._time = 0
282
+ }
283
+ }