@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.
- package/LICENSE +202 -0
- package/README.md +777 -0
- package/dist/index.d.ts +8144 -0
- package/dist/index.js +26231 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -0
- package/src/cognition/agency/access.grants.ts +59 -0
- package/src/cognition/agency/competence.codec.ts +89 -0
- package/src/cognition/agency/engines/action.selector.ts +462 -0
- package/src/cognition/agency/engines/affordance.synthesizer.ts +384 -0
- package/src/cognition/agency/engines/deliberation.engine.ts +241 -0
- package/src/cognition/agency/engines/instruction.intake.ts +54 -0
- package/src/cognition/agency/engines/motor.schema.executor.ts +532 -0
- package/src/cognition/agency/engines/reafference.engine.ts +241 -0
- package/src/cognition/agency/execution.primitives.ts +118 -0
- package/src/cognition/agency/index.ts +58 -0
- package/src/cognition/agency/proactive.communicator.ts +274 -0
- package/src/cognition/agency/reconcile.learning.ts +82 -0
- package/src/cognition/agency/schemas/external.ts +59 -0
- package/src/cognition/agency/schemas/innate.ts +111 -0
- package/src/cognition/agency/schemas/repertoire.ts +246 -0
- package/src/cognition/agency/selection.scoring.ts +191 -0
- package/src/cognition/agency/types.ts +165 -0
- package/src/cognition/bus.ts +391 -0
- package/src/cognition/completion.inbox.ts +86 -0
- package/src/cognition/config.mirror.entities.ts +475 -0
- package/src/cognition/conversation.memory.ts +81 -0
- package/src/cognition/event.log.ts +116 -0
- package/src/cognition/event.schemas.ts +568 -0
- package/src/cognition/faculties/aesthetic.evaluator.ts +313 -0
- package/src/cognition/faculties/affective.blender.ts +562 -0
- package/src/cognition/faculties/attachment.evaluator.ts +432 -0
- package/src/cognition/faculties/attention.allocator.ts +422 -0
- package/src/cognition/faculties/autobiographical.narrator.ts +300 -0
- package/src/cognition/faculties/bias.detector.ts +363 -0
- package/src/cognition/faculties/circadian.oscillator.ts +294 -0
- package/src/cognition/faculties/confidence.calibrator.ts +324 -0
- package/src/cognition/faculties/dream.simulator.ts +251 -0
- package/src/cognition/faculties/empathy.simulator.ts +253 -0
- package/src/cognition/faculties/energy.regulator.ts +320 -0
- package/src/cognition/faculties/episodic.consolidator.ts +728 -0
- package/src/cognition/faculties/executive.engine/commands.ts +459 -0
- package/src/cognition/faculties/executive.engine/config.ts +46 -0
- package/src/cognition/faculties/executive.engine/context.ts +605 -0
- package/src/cognition/faculties/executive.engine/deferred.effects.ts +104 -0
- package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +60 -0
- package/src/cognition/faculties/executive.engine/effort.gate.ts +114 -0
- package/src/cognition/faculties/executive.engine/engine.ts +1028 -0
- package/src/cognition/faculties/executive.engine/escalation.buffer.ts +90 -0
- package/src/cognition/faculties/executive.engine/facet.supervisor.ts +271 -0
- package/src/cognition/faculties/executive.engine/facet.ts +628 -0
- package/src/cognition/faculties/executive.engine/gating.ts +222 -0
- package/src/cognition/faculties/executive.engine/index.ts +6 -0
- package/src/cognition/faculties/executive.engine/messages.ts +102 -0
- package/src/cognition/faculties/executive.engine/parser.ts +380 -0
- package/src/cognition/faculties/executive.engine/prompt.factory.ts +1053 -0
- package/src/cognition/faculties/executive.engine/types.ts +323 -0
- package/src/cognition/faculties/exteroception.ts +338 -0
- package/src/cognition/faculties/forgetting.curve.ts +202 -0
- package/src/cognition/faculties/frustration.evaluator.ts +280 -0
- package/src/cognition/faculties/goal.manager.ts +1008 -0
- package/src/cognition/faculties/inhibition.controller.ts +351 -0
- package/src/cognition/faculties/interoception.ts +417 -0
- package/src/cognition/faculties/introspection.engine.ts +246 -0
- package/src/cognition/faculties/known.entity.tracker.ts +439 -0
- package/src/cognition/faculties/loss.evaluator.ts +224 -0
- package/src/cognition/faculties/moral.evaluator.ts +437 -0
- package/src/cognition/faculties/novelty.detector.ts +254 -0
- package/src/cognition/faculties/persona.consolidator.ts +845 -0
- package/src/cognition/faculties/planning.engine/engine.ts +859 -0
- package/src/cognition/faculties/planning.engine/plan.frontier.ts +83 -0
- package/src/cognition/faculties/planning.engine/plan.store.ts +174 -0
- package/src/cognition/faculties/planning.engine/plan.supervision.ts +494 -0
- package/src/cognition/faculties/planning.engine/types.ts +143 -0
- package/src/cognition/faculties/reputation.tracker.ts +294 -0
- package/src/cognition/faculties/reward.evaluator.ts +304 -0
- package/src/cognition/faculties/self.model.updater.ts +677 -0
- package/src/cognition/faculties/semantic.engine/clustering.ts +673 -0
- package/src/cognition/faculties/semantic.engine/index.ts +6 -0
- package/src/cognition/faculties/semantic.engine/integrator.ts +750 -0
- package/src/cognition/faculties/semantic.engine/types.ts +53 -0
- package/src/cognition/faculties/sleep.pressure.regulator.ts +262 -0
- package/src/cognition/faculties/social.perception.ts +333 -0
- package/src/cognition/faculties/spaced.repetition.ts +605 -0
- package/src/cognition/faculties/stress.regulator.ts +375 -0
- package/src/cognition/faculties/task.switcher.ts +239 -0
- package/src/cognition/faculties/theory.of.mind.ts +333 -0
- package/src/cognition/faculties/threat.evaluator.ts +312 -0
- package/src/cognition/faculties/working.memory.ts +406 -0
- package/src/cognition/generative.model.ts +344 -0
- package/src/cognition/heartbeat.ts +70 -0
- package/src/cognition/index.ts +291 -0
- package/src/cognition/instruction.handler.ts +146 -0
- package/src/cognition/memory/index.ts +35 -0
- package/src/cognition/memory/vector.adapter.ts +316 -0
- package/src/cognition/memory/vector.content.ts +47 -0
- package/src/cognition/memory/vector.embedder.ts +212 -0
- package/src/cognition/memory/vector.index.ts +459 -0
- package/src/cognition/memory/vector.types.ts +65 -0
- package/src/cognition/orchestrator.ts +142 -0
- package/src/cognition/persona.prior.ts +265 -0
- package/src/cognition/schema.registry.ts +145 -0
- package/src/cognition/senses/audition.engine/engine.ts +976 -0
- package/src/cognition/senses/audition.engine/salience.ts +59 -0
- package/src/cognition/senses/base.sense.engine.ts +117 -0
- package/src/cognition/senses/gustation.engine.ts +24 -0
- package/src/cognition/senses/index.ts +172 -0
- package/src/cognition/senses/olfaction.engine.ts +24 -0
- package/src/cognition/senses/somatosensation.engine.ts +23 -0
- package/src/cognition/senses/vision.engine.ts +23 -0
- package/src/cognition/types.ts +63 -0
- package/src/cognition/utilities/token.tracker.ts +492 -0
- package/src/core/abstracts.ts +60 -0
- package/src/core/async.engine.ts +539 -0
- package/src/core/clock.ts +196 -0
- package/src/core/completion.recorder.ts +122 -0
- package/src/core/conflict.detector.ts +140 -0
- package/src/core/distributed.ts +626 -0
- package/src/core/event.bus.ts +292 -0
- package/src/core/inbound.recorder.ts +100 -0
- package/src/core/index.ts +167 -0
- package/src/core/logger.ts +85 -0
- package/src/core/metrics.ts +107 -0
- package/src/core/orchestrator.ts +625 -0
- package/src/core/replay.ts +589 -0
- package/src/core/scenario.ts +120 -0
- package/src/core/serialization.ts +448 -0
- package/src/core/simulation.ts +197 -0
- package/src/core/snapshot.manager.ts +293 -0
- package/src/core/state.manager.ts +283 -0
- package/src/core/types.ts +274 -0
- package/src/core/utils.ts +82 -0
- package/src/core/wall.clock.ts +23 -0
- package/src/extensions/livestream.ext.ts +570 -0
- package/src/extensions/time.ext.ts +339 -0
- package/src/index.ts +91 -0
- package/src/llm/gate.ts +140 -0
- package/src/llm/index.ts +716 -0
- package/src/llm/summarizer.ts +170 -0
- package/src/llm/wire.contracts.ts +67 -0
- package/src/pma/eval.ts +651 -0
- package/src/pma/index.ts +1082 -0
- package/src/profiles/built-in.ts +7 -0
- package/src/profiles/companion.ts +28 -0
- package/src/profiles/company-brain.ts +54 -0
- package/src/profiles/customer-service.ts +30 -0
- package/src/profiles/game-npc.ts +20 -0
- package/src/profiles/index.ts +42 -0
- package/src/profiles/smart-home.ts +34 -0
- package/src/runners/coherence.runner.ts +49 -0
- package/src/runners/outreach.runner.ts +158 -0
- package/src/runners/social.runner.ts +182 -0
- package/src/runners/thin-shim.runner.ts +195 -0
- package/src/sdk/will.ts +334 -0
- package/src/stem/assembly.audit.ts +88 -0
- package/src/stem/distribution.ts +203 -0
- package/src/stem/guards/identity.coherence.ts +152 -0
- package/src/stem/guards/identity.guard.ts +227 -0
- package/src/stem/index.ts +1145 -0
- package/src/stem/mind.ts +1090 -0
- package/src/stem/tracts/ack.reconciler.ts +55 -0
- package/src/stem/tracts/biography.writer.ts +190 -0
- package/src/stem/tracts/effector.controller.ts +143 -0
- package/src/stem/tracts/health.reporter.ts +113 -0
- package/src/stem/tracts/inbound.queue.ts +54 -0
- package/src/stem/tracts/outbox.controller.ts +167 -0
- package/src/stem/tracts/outbox.writer.ts +133 -0
- package/src/stem/tracts/pma.controller.ts +109 -0
- package/src/stem/tracts/replay.controller.ts +177 -0
- package/src/stem/tracts/sensory.controller.ts +168 -0
- package/src/stem/tracts/session.logger.ts +157 -0
- package/src/stem/tracts/transport/index.ts +38 -0
- package/src/stem/tracts/transport/loopback.transport.ts +86 -0
- package/src/stem/tracts/transport/socketio.transport.ts +200 -0
- package/src/stem/tracts/transport/stream.transport.ts +152 -0
- package/src/stem/tracts/transport/types.ts +193 -0
- package/src/stem/tracts/transport.controller.ts +380 -0
- 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
|
+
}
|