@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,448 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/core/serialization.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ /**
6
+ * Serialization/Deserialization for simulation state.
7
+ * Supports JSON and binary (length-prefixed JSON, upgradeable to MessagePack).
8
+ */
9
+
10
+ import type { SimulationState, SimulationEntity, Tick, Timestamp } from '#core/types'
11
+ import { BunStorageAdapter, type StorageAdapter } from '#core/abstracts'
12
+ import { wallClock } from '#core/wall.clock'
13
+
14
+ export type SerializationFormat = 'json' | 'binary'
15
+
16
+ export interface SerializedEntity {
17
+ id: string
18
+ type: string
19
+ createdAt: number
20
+ updatedAt: number
21
+ /** Sim tick of the last write — needed by ConflictDetector after a restore. */
22
+ updatedAtTick?: number
23
+ metadata?: Record<string, unknown>
24
+ components: Record<string, unknown>
25
+ }
26
+
27
+ export interface SerializedState {
28
+ version: string
29
+ format: SerializationFormat
30
+ schema: string
31
+ tick: Tick
32
+ time: Timestamp
33
+ entities: SerializedEntity[]
34
+ metrics: Array<[string, number]>
35
+ createdAt: Timestamp
36
+ checksum?: string
37
+ }
38
+
39
+ export interface Serializer {
40
+ serialize( state: SimulationState, format?: SerializationFormat ): string | Uint8Array
41
+ deserialize( data: string | Uint8Array, format?: SerializationFormat ): SimulationState
42
+ export( state: SimulationState, path: string ): Promise<void>
43
+ import( path: string ): Promise<SimulationState>
44
+ }
45
+
46
+ export interface SerializationConfig {
47
+ format?: SerializationFormat
48
+ compress?: boolean
49
+ includeChecksum?: boolean
50
+ schemaVersion?: string
51
+ prettyPrint?: boolean
52
+ storage?: StorageAdapter
53
+ }
54
+
55
+ export class DefaultSerializer implements Serializer {
56
+ private _config: SerializationConfig
57
+ private _encoder = new TextEncoder()
58
+ private _decoder = new TextDecoder()
59
+ private _storage: StorageAdapter
60
+
61
+ constructor( config: SerializationConfig = {} ){
62
+ this._config = {
63
+ format: config.format ?? 'json',
64
+ compress: config.compress ?? false,
65
+ includeChecksum: config.includeChecksum ?? true,
66
+ schemaVersion: config.schemaVersion ?? '1.0.0',
67
+ prettyPrint: config.prettyPrint ?? false
68
+ }
69
+
70
+ this._storage = config.storage ?? new BunStorageAdapter()
71
+ }
72
+
73
+ serialize( state: SimulationState, format: SerializationFormat = this._config.format! ): string | Uint8Array {
74
+ const serialized = this._toSerialized( state )
75
+
76
+ switch( format ){
77
+ case 'json': return this._config.prettyPrint
78
+ ? JSON.stringify( serialized, null, 2 )
79
+ : JSON.stringify( serialized )
80
+
81
+ case 'binary': return this._toBinary( serialized )
82
+ default: throw new Error(`Unknown format: ${format}`)
83
+ }
84
+ }
85
+
86
+ deserialize( data: string | Uint8Array, _format?: SerializationFormat ): SimulationState {
87
+ let serialized: SerializedState
88
+
89
+ if( typeof data === 'string')
90
+ serialized = JSON.parse( data ) as SerializedState
91
+
92
+ else {
93
+ const firstByte = data[0]
94
+ serialized = firstByte === 0x7B // '{' in ASCII — JSON wrapped in bytes
95
+ ? JSON.parse( this._decoder.decode( data ) )
96
+ : this._fromBinary( data )
97
+ }
98
+
99
+ return this._fromSerialized( serialized )
100
+ }
101
+
102
+ async export( state: SimulationState, path: string ): Promise<void> {
103
+ const
104
+ serialized = this.serialize( state ),
105
+ content = typeof serialized === 'string'
106
+ ? serialized
107
+ : JSON.stringify( Array.from( serialized ) )
108
+
109
+ await this._storage.write( path, content )
110
+ }
111
+
112
+ async import( path: string ): Promise<SimulationState> {
113
+ if( !( await this._storage.exists( path ) ) )
114
+ throw new Error(`File not found: ${path}`)
115
+
116
+ const content = await this._storage.read( path )
117
+ return this.deserialize( content )
118
+ }
119
+
120
+ // ── Serialization ────────────────────────────────────────
121
+
122
+ private _toSerialized( state: SimulationState ): SerializedState {
123
+ const entities: SerializedEntity[] = []
124
+
125
+ for( const entity of state.entities.values() ){
126
+ const components: Record<string, unknown> = entity.metadata?.components as Record<string, unknown> ?? {}
127
+
128
+ entities.push({
129
+ id: entity.id,
130
+ type: entity.type,
131
+ createdAt: entity.createdAt,
132
+ updatedAt: entity.updatedAt,
133
+ updatedAtTick: entity.updatedAtTick,
134
+ // metadata/components are free-form `Record<string, unknown>`; encode so
135
+ // Map/Set/Date/undefined survive the JSON round-trip instead of being
136
+ // silently flattened or dropped (FN15). Plain JSON values pass through.
137
+ metadata: entity.metadata === undefined
138
+ ? undefined
139
+ : this._encodeSpecial( entity.metadata ) as Record<string, unknown>,
140
+ components: this._encodeSpecial( components ) as Record<string, unknown>,
141
+ })
142
+ }
143
+
144
+ // Build base without checksum first — checksum must be computed
145
+ // over a stable object that does not include the checksum field itself.
146
+ const base: Omit<SerializedState, 'checksum'> = {
147
+ version: this._config.schemaVersion!,
148
+ format: this._config.format!,
149
+ schema: 'simulation-state-v1',
150
+ tick: state.tick,
151
+ time: state.time,
152
+ entities,
153
+ metrics: Array.from( state.metrics.entries() ),
154
+ createdAt: wallClock(),
155
+ }
156
+
157
+ return this._config.includeChecksum
158
+ ? { ...base, checksum: this._computeChecksum( base ) }
159
+ : base as SerializedState
160
+ }
161
+
162
+ private _fromSerialized( serialized: SerializedState ): SimulationState {
163
+ if( this._config.includeChecksum && serialized.checksum ){
164
+ // Destructure checksum out before verifying — the hash must be computed
165
+ // over the same field set used during write (i.e. without the checksum field).
166
+ const
167
+ { checksum, ...rest } = serialized,
168
+ computed = this._computeChecksum( rest )
169
+
170
+ if( computed !== checksum )
171
+ throw new Error('Checksum mismatch — data may be corrupted')
172
+ }
173
+
174
+ const entities = new Map<string, SimulationEntity>()
175
+ for( const e of serialized.entities ){
176
+ // Reverse the FN15 encoding: revive Map/Set/Date/undefined that were
177
+ // tagged on the way out.
178
+ const metadata = e.metadata === undefined
179
+ ? undefined
180
+ : this._decodeSpecial( e.metadata ) as Record<string, unknown>
181
+ const components = this._decodeSpecial( e.components ) as Record<string, unknown>
182
+
183
+ entities.set( e.id, {
184
+ id: e.id,
185
+ type: e.type,
186
+ createdAt: e.createdAt,
187
+ updatedAt: e.updatedAt,
188
+ updatedAtTick: e.updatedAtTick,
189
+ metadata: { ...metadata, components },
190
+ })
191
+ }
192
+
193
+ return {
194
+ tick: serialized.tick,
195
+ time: serialized.time,
196
+ entities,
197
+ metrics: new Map( serialized.metrics ),
198
+ }
199
+ }
200
+
201
+ // ── Special-value encoding (FN15) ────────────────────────
202
+ //
203
+ // Entity metadata/components are free-form `Record<string, unknown>`. Plain
204
+ // `JSON.stringify` silently loses anything that is not JSON-native: `Map` and
205
+ // `Set` flatten to `{}`, `Date` becomes a string that never revives, and
206
+ // `undefined`-valued keys vanish. We tag those values on the way out and
207
+ // revive them on the way in so the round-trip is lossless.
208
+ //
209
+ // Encoding is a reserved-key scheme: `{ __ser: <kind>, __val: <payload> }`.
210
+ // Objects that already carry a `__ser` string key are a (documented) edge we
211
+ // accept — metadata is engine-internal, not user-controlled.
212
+
213
+ private _encodeSpecial( value: unknown ): unknown {
214
+ if( value === undefined ) return { __ser: 'undefined' }
215
+ if( value === null ) return null
216
+ if( value instanceof Date ) return { __ser: 'date', __val: value.toISOString() }
217
+ if( value instanceof Map )
218
+ return { __ser: 'map', __val: Array.from( value.entries() )
219
+ .map( ([ k, v ]) => [ this._encodeSpecial( k ), this._encodeSpecial( v ) ] ) }
220
+ if( value instanceof Set )
221
+ return { __ser: 'set', __val: Array.from( value.values() ).map( v => this._encodeSpecial( v ) ) }
222
+ if( Array.isArray( value ) )
223
+ return value.map( v => this._encodeSpecial( v ) )
224
+
225
+ if( typeof value === 'object' ){
226
+ const out: Record<string, unknown> = {}
227
+ for( const [ k, v ] of Object.entries( value as Record<string, unknown> ) )
228
+ out[k] = this._encodeSpecial( v )
229
+ return out
230
+ }
231
+
232
+ return value // string | number | boolean (bigint/symbol/function unchanged from prior behaviour)
233
+ }
234
+
235
+ private _decodeSpecial( value: unknown ): unknown {
236
+ if( value === null || typeof value !== 'object' ) return value
237
+ if( Array.isArray( value ) ) return value.map( v => this._decodeSpecial( v ) )
238
+
239
+ const tag = ( value as Record<string, unknown> ).__ser
240
+ if( typeof tag === 'string' ){
241
+ const payload = ( value as Record<string, unknown> ).__val
242
+ switch( tag ){
243
+ case 'undefined': return undefined
244
+ case 'date': return new Date( payload as string ) // determinism-ok: reviving a stored Date, not reading current time
245
+ case 'map': return new Map(
246
+ ( payload as unknown[] ).map( pair => {
247
+ const [ k, v ] = pair as [ unknown, unknown ]
248
+ return [ this._decodeSpecial( k ), this._decodeSpecial( v ) ] as [ unknown, unknown ]
249
+ })
250
+ )
251
+ case 'set': return new Set( ( payload as unknown[] ).map( v => this._decodeSpecial( v ) ) )
252
+ }
253
+ }
254
+
255
+ const out: Record<string, unknown> = {}
256
+ for( const [ k, v ] of Object.entries( value as Record<string, unknown> ) )
257
+ out[k] = this._decodeSpecial( v )
258
+ return out
259
+ }
260
+
261
+ // ── Binary encoding ──────────────────────────────────────
262
+
263
+ private _toBinary( state: SerializedState ): Uint8Array {
264
+ // Length-prefixed JSON (uint32 big-endian header).
265
+ // Upgrade path: swap JSON.stringify for MessagePack/CBOR without
266
+ // changing the framing protocol.
267
+ const
268
+ json = JSON.stringify( state ),
269
+ bytes = this._encoder.encode( json ),
270
+ result = new Uint8Array( bytes.length + 4 )
271
+
272
+ result[0] = ( bytes.length >> 24 ) & 0xFF
273
+ result[1] = ( bytes.length >> 16 ) & 0xFF
274
+ result[2] = ( bytes.length >> 8 ) & 0xFF
275
+ result[3] = bytes.length & 0xFF
276
+
277
+ result.set( bytes, 4 )
278
+ return result
279
+ }
280
+
281
+ private _fromBinary( data: Uint8Array ): SerializedState {
282
+ if( data.length < 4 )
283
+ throw new Error('Invalid binary data: too short')
284
+
285
+ const length =
286
+ ( ( data[0] ?? 0 ) << 24 ) |
287
+ ( ( data[1] ?? 0 ) << 16 ) |
288
+ ( ( data[2] ?? 0 ) << 8 ) |
289
+ ( data[3] ?? 0 )
290
+
291
+ if( data.length - 4 < length )
292
+ throw new Error('Invalid binary data: length mismatch')
293
+
294
+ const json = this._decoder.decode( data.slice( 4, 4 + length ) )
295
+ return JSON.parse( json ) as SerializedState
296
+ }
297
+
298
+ // ── Checksum ─────────────────────────────────────────────
299
+
300
+ /**
301
+ * djb2 hash over meaningful content fields.
302
+ *
303
+ * Coverage:
304
+ * - tick + time → catches clock corruption
305
+ * - entity id:type:updatedAt + encoded metadata/components per entity
306
+ * → catches add/remove/mutate AND (de)serialization round-trip loss
307
+ * - metric key:value per entry → catches any metric corruption
308
+ *
309
+ * metadata/components are now hashed directly (FN15): excluding them meant a
310
+ * value silently dropped on serialization (a Map flattening to `{}`, a missing
311
+ * `undefined` key) left `updatedAt` unchanged and so passed the integrity check
312
+ * undetected. The values are hashed in their already-encoded form, so the
313
+ * checksum computed at serialize time matches the one verified at deserialize
314
+ * time (the decode happens only after verification).
315
+ */
316
+ private _computeChecksum( state: Omit<SerializedState, 'checksum'> ): string {
317
+ let hash = 0
318
+
319
+ // Timeline anchor — catches tick/time tampering
320
+ const header = `${state.tick}:${state.time}:${state.entities.length}:${state.metrics.length}`
321
+ for( let i = 0; i < header.length; i++ ){
322
+ hash = ( ( hash << 5 ) - hash ) + header.charCodeAt( i )
323
+ hash |= 0
324
+ }
325
+
326
+ // Per-entity identity, mutation timestamp, and (encoded) metadata/components
327
+ for( const e of state.entities ){
328
+ const str = `${e.id}:${e.type}:${e.updatedAt}:${e.updatedAtTick}:${JSON.stringify( e.metadata )}:${JSON.stringify( e.components )}`
329
+ for( let i = 0; i < str.length; i++ ){
330
+ hash = ( ( hash << 5 ) - hash ) + str.charCodeAt( i )
331
+ hash |= 0
332
+ }
333
+ }
334
+
335
+ // Full metric key-value coverage
336
+ for( const [ key, value ] of state.metrics ){
337
+ const str = `${key}:${value}`
338
+ for( let i = 0; i < str.length; i++ ){
339
+ hash = ( ( hash << 5 ) - hash ) + str.charCodeAt( i )
340
+ hash |= 0
341
+ }
342
+ }
343
+
344
+ return ( hash >>> 0 ).toString( 16 )
345
+ }
346
+ }
347
+
348
+ // ── Delta encoding ───────────────────────────────────────────
349
+
350
+ export interface DeltaSnapshot {
351
+ baseTick: Tick
352
+ currentTick: Tick
353
+ /**
354
+ * The simulation `time` recorded at `currentTick`. Captured at encode time so
355
+ * decode reconstructs the real timestamp instead of fabricating one from a
356
+ * hardcoded tick interval (FN14): the clock's per-tick duration is
357
+ * configurable (`fixedDeltaMs`) and scaled (`timeScale`), so `time` is not a
358
+ * fixed multiple of the tick number.
359
+ */
360
+ time: Timestamp
361
+ addedEntities: SimulationEntity[]
362
+ removedEntityIds: string[]
363
+ updatedEntities: Array<{ id: string; changes: Partial<SimulationEntity> }>
364
+ metricsDelta: Array<[string, number]>
365
+ }
366
+
367
+ export class DeltaEncoder {
368
+ encode( previous: SimulationState, current: SimulationState ): DeltaSnapshot {
369
+ const
370
+ addedEntities: SimulationEntity[] = [],
371
+ removedEntityIds: string[] = [],
372
+ updatedEntities: Array<{ id: string; changes: Partial<SimulationEntity> }> = []
373
+
374
+ for( const id of previous.entities.keys() )
375
+ !current.entities.has( id ) && removedEntityIds.push( id )
376
+
377
+ for( const [ id, currentEntity ] of current.entities ){
378
+ const previousEntity = previous.entities.get( id )
379
+ if( !previousEntity ){
380
+ addedEntities.push( currentEntity )
381
+ continue
382
+ }
383
+
384
+ if( JSON.stringify( previousEntity ) !== JSON.stringify( currentEntity ) ){
385
+ const changes: Partial<SimulationEntity> = {}
386
+
387
+ if( previousEntity.type !== currentEntity.type ) changes.type = currentEntity.type
388
+ if( previousEntity.updatedAt !== currentEntity.updatedAt ) changes.updatedAt = currentEntity.updatedAt
389
+ if( JSON.stringify( previousEntity.metadata ) !== JSON.stringify( currentEntity.metadata ) )
390
+ changes.metadata = currentEntity.metadata
391
+
392
+ updatedEntities.push({ id, changes })
393
+ }
394
+ }
395
+
396
+ const metricsDelta: Array<[string, number]> = []
397
+ for( const [ key, currentValue ] of current.metrics ){
398
+ const previousValue = previous.metrics.get( key )
399
+ previousValue !== currentValue && metricsDelta.push([ key, currentValue - ( previousValue ?? 0 ) ])
400
+ }
401
+
402
+ for( const [ key ] of previous.metrics )
403
+ !current.metrics.has( key ) && metricsDelta.push([ key, -Infinity ])
404
+
405
+ return {
406
+ baseTick: previous.tick,
407
+ currentTick: current.tick,
408
+ time: current.time,
409
+ addedEntities,
410
+ removedEntityIds,
411
+ updatedEntities,
412
+ metricsDelta,
413
+ }
414
+ }
415
+
416
+ decode( base: SimulationState, delta: DeltaSnapshot ): SimulationState {
417
+ const
418
+ entities = new Map( base.entities ),
419
+ metrics = new Map( base.metrics )
420
+
421
+ for( const id of delta.removedEntityIds )
422
+ entities.delete( id )
423
+
424
+ for( const entity of delta.addedEntities )
425
+ entities.set( entity.id, entity )
426
+
427
+ for( const { id, changes } of delta.updatedEntities ){
428
+ const existing = entities.get( id )
429
+ existing && entities.set( id, { ...existing, ...changes } )
430
+ }
431
+
432
+ for( const [ key, deltaValue ] of delta.metricsDelta ){
433
+ deltaValue === -Infinity
434
+ ? metrics.delete( key )
435
+ : metrics.set( key, ( metrics.get( key ) ?? 0 ) + deltaValue )
436
+ }
437
+
438
+ return {
439
+ tick: delta.currentTick,
440
+ // Use the timestamp recorded at encode time. Falls back to the base time
441
+ // only for legacy deltas that predate the `time` field — never the old
442
+ // fabricated `+ (Δtick) * 16` value (FN14).
443
+ time: delta.time ?? base.time,
444
+ entities,
445
+ metrics
446
+ }
447
+ }
448
+ }
@@ -0,0 +1,197 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/core/simulation.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import type { Scenario } from '#core/scenario'
6
+ import type { SimulationContext, SimulationState, SeededPRNG, RestoreOptions } from '#core/types'
7
+
8
+ import { DefaultStateManager, type StateManager } from '#core/state.manager'
9
+ import { DefaultMetricCollector, type MetricCollector } from '#core/metrics'
10
+ import { SnapshotManager, type SnapshotManagerConfig } from '#core/snapshot.manager'
11
+ import { DefaultEventBus, type EventBus, type EventBusConfig } from '#core/event.bus'
12
+ import { DefaultSimulationClock, type SimulationClock, type ClockConfig } from '#core/clock'
13
+ import { DefaultOrchestrator, type TickMiddleware, type Orchestrator, type OrchestratorConfig, type SimulationEngine } from '#core/orchestrator'
14
+ import { createPRNG } from '#core/utils'
15
+ import { wallClock } from '#core/wall.clock'
16
+
17
+ export interface Simulation {
18
+ readonly clock: SimulationClock
19
+ readonly eventBus: EventBus
20
+ readonly stateManager: StateManager
21
+ readonly orchestrator: Orchestrator
22
+ readonly metrics: MetricCollector
23
+ readonly context: SimulationContext
24
+ readonly randomSeed: number
25
+
26
+ addEngine( engine: SimulationEngine ): void
27
+ loadScenario( scenario: Scenario ): Promise<void>
28
+ run(): Promise<void>
29
+ stop(): void
30
+ pause(): void
31
+ resume(): void
32
+ step( count?: number ): Promise<void>
33
+
34
+ /** Takes a snapshot of current state and returns its unique ID. */
35
+ snapshot(): string
36
+ /**
37
+ * Use { entities: false, metrics: true } to keep current entities
38
+ * while rolling back only metrics.
39
+ */
40
+ restore( snapshotId: string, options?: RestoreOptions ): Promise<boolean>
41
+ }
42
+
43
+ export interface SimulationConfig {
44
+ clock?: ClockConfig
45
+ eventBus?: EventBusConfig
46
+ orchestrator?: OrchestratorConfig
47
+ metricsAutoFlushMs?: number
48
+ snapshot?: SnapshotManagerConfig
49
+ context?: Partial<Omit<SimulationContext, 'prng'>>
50
+ /** Seed for the PRNG. Defaults to wallClock() — record it to replay. */
51
+ randomSeed?: number
52
+ /**
53
+ * Optional factory that replaces DefaultOrchestrator with a custom implementation.
54
+ *
55
+ * The simulation constructs clock, eventBus, and stateManager first, then
56
+ * passes them — along with the resolved OrchestratorConfig (including the
57
+ * merged onAfterTick handlers) — into this factory.
58
+ *
59
+ * The cognitive layer uses this to inject CognitiveOrchestrator without
60
+ * creating any import dependency from core → cognition.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * orchestratorFactory: (clock, eventBus, stateManager, cfg) =>
65
+ * new CognitiveOrchestrator(clock, eventBus, stateManager, cfg)
66
+ * ```
67
+ */
68
+ orchestratorFactory?: (
69
+ clock: SimulationClock,
70
+ eventBus: EventBus,
71
+ stateManager: StateManager,
72
+ config: OrchestratorConfig,
73
+ ) => Orchestrator
74
+ }
75
+
76
+ export class DefaultSimulation implements Simulation {
77
+ readonly clock: SimulationClock
78
+ readonly eventBus: EventBus
79
+ readonly stateManager: StateManager
80
+ readonly snapshotManager: SnapshotManager
81
+ readonly orchestrator: Orchestrator
82
+ readonly metrics: DefaultMetricCollector
83
+ readonly context: SimulationContext
84
+ readonly randomSeed: number
85
+
86
+ private _scenario: Scenario | null = null
87
+ private _snapshots: Map<string, SimulationState> = new Map()
88
+
89
+ constructor( config: SimulationConfig = {} ){
90
+ const
91
+ seed: number = config.randomSeed ?? wallClock(),
92
+ prng: SeededPRNG = createPRNG( seed )
93
+
94
+ this.randomSeed = seed
95
+ this.clock = new DefaultSimulationClock( config.clock )
96
+ // Stamp event timestamps from the sim clock (not wall time) so the event
97
+ // log is reproducible on replay (R2). Caller-supplied `now` wins if set.
98
+ this.eventBus = new DefaultEventBus( { now: () => this.clock.now, ...config.eventBus } )
99
+ this.stateManager = new DefaultStateManager()
100
+ this.snapshotManager = new SnapshotManager( config.snapshot )
101
+ this.metrics = new DefaultMetricCollector( config.metricsAutoFlushMs ?? 5000 )
102
+
103
+ /**
104
+ * Both metrics and snapshots are core concerns — registered as
105
+ * onAfterTick handlers in the orchestrator config.
106
+ *
107
+ * The orchestrator calls them in registration order:
108
+ * 1. metrics.onTick — captures metric points
109
+ * 2. snapshotManager.onTick — serializes state, feeds replay
110
+ * 3. User-provided onAfterTick handlers (if any)
111
+ */
112
+ const coreAfterTick: TickMiddleware[] = [
113
+ this.metrics.onTick,
114
+ this.snapshotManager.onTick,
115
+ ]
116
+
117
+ // Merge user-provided onAfterTick handlers
118
+ if( config.orchestrator?.onAfterTick ){
119
+ const userHandlers = Array.isArray( config.orchestrator.onAfterTick )
120
+ ? config.orchestrator.onAfterTick
121
+ : [ config.orchestrator.onAfterTick ]
122
+
123
+ coreAfterTick.push( ...userHandlers )
124
+ }
125
+
126
+ const orchestratorConfig: OrchestratorConfig = {
127
+ ...config.orchestrator,
128
+ onAfterTick: coreAfterTick,
129
+ }
130
+
131
+ this.orchestrator = config.orchestratorFactory
132
+ ? config.orchestratorFactory( this.clock, this.eventBus, this.stateManager, orchestratorConfig )
133
+ : new DefaultOrchestrator( this.clock, this.eventBus, this.stateManager, orchestratorConfig )
134
+
135
+ this.context = {
136
+ // Run-identity metadata, not replay/snapshot state: a brand-new run with no
137
+ // caller-supplied id needs a unique handle; replay passes ids in via config.
138
+ simulationId: config.context?.simulationId ?? crypto.randomUUID(), // determinism-ok: run-identity metadata
139
+ runId: config.context?.runId ?? crypto.randomUUID(), // determinism-ok: run-identity metadata
140
+ tags: config.context?.tags ?? {},
141
+ prng
142
+ }
143
+ }
144
+
145
+ addEngine( engine: SimulationEngine ): void {
146
+ this.orchestrator.addEngine( engine )
147
+ }
148
+
149
+ async loadScenario( scenario: Scenario ): Promise<void> {
150
+ const validation = scenario.validate()
151
+ if( !validation.isValid )
152
+ throw new Error(`Invalid scenario: ${validation.errors.join(', ')}`)
153
+
154
+ this._scenario = scenario
155
+
156
+ await scenario.initialize( this.stateManager, this.context )
157
+ }
158
+
159
+ async run(): Promise<void> {
160
+ if( !this._scenario )
161
+ throw new Error('No scenario loaded. Call loadScenario() first.')
162
+
163
+ await this.orchestrator.start( this.context )
164
+ }
165
+
166
+ stop(): void { this.orchestrator.stop() }
167
+ pause(): void { this.orchestrator.pause() }
168
+ resume(): void { this.orchestrator.resume() }
169
+
170
+ async step( count: number = 1 ): Promise<void> {
171
+ await this.orchestrator.step( count, {
172
+ simulationId: this.context.simulationId,
173
+ runId: this.context.runId
174
+ })
175
+ }
176
+
177
+ snapshot(): string {
178
+ const
179
+ state = this.stateManager.snapshot(),
180
+ snapshotId = `snapshot_${this.context.runId}_${this.clock.currentTick}_${wallClock()}`
181
+
182
+ this._snapshots.set( snapshotId, state )
183
+ return snapshotId
184
+ }
185
+
186
+ /**
187
+ * Forwards RestoreOptions to StateManager
188
+ * for partial restore capability.
189
+ */
190
+ async restore( snapshotId: string, options?: RestoreOptions ): Promise<boolean> {
191
+ const snapshot = this._snapshots.get( snapshotId )
192
+ if( !snapshot ) return false
193
+
194
+ this.stateManager.restore( snapshot, options )
195
+ return true
196
+ }
197
+ }