@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,195 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/stem/runner.ts — Development entry point
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// Thin shim around WillStem for local development.
|
|
6
|
+
// Reads config from env vars, starts a single Will, and prints
|
|
7
|
+
// a human-readable status line every LOG_INTERVAL ticks.
|
|
8
|
+
//
|
|
9
|
+
// Production code (API server) imports WillStem directly and
|
|
10
|
+
// drives the same lifecycle without this file.
|
|
11
|
+
//
|
|
12
|
+
// Usage:
|
|
13
|
+
// bun run src/stem/runner.ts
|
|
14
|
+
// WILL_ENGINE_TIER=full WILL_MAX_TICKS=200 bun run src/stem/runner.ts
|
|
15
|
+
// ─────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
import type { WillConfig } from '#stem/mind'
|
|
18
|
+
import type { StateSnapshot } from '#stem/index'
|
|
19
|
+
import { WillStem } from '#stem/index'
|
|
20
|
+
|
|
21
|
+
// ── Env config ────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const WILL_ID = process.env.WILL_ID ?? 'demo'
|
|
24
|
+
const WILL_NAME = process.env.WILL_NAME ?? 'William'
|
|
25
|
+
const TICK_MS = parseInt( process.env.WILL_TICK_MS ?? '1000')
|
|
26
|
+
const MAX_TICKS = parseInt( process.env.WILL_MAX_TICKS ?? '0' )
|
|
27
|
+
const LOG_INTERVAL = parseInt( process.env.WILL_LOG_INTERVAL ?? '10' )
|
|
28
|
+
const RANDOM_SEED = parseInt( process.env.WILL_SEED ?? String( Date.now() ) )
|
|
29
|
+
const ENGINE_TIER = ( process.env.WILL_ENGINE_TIER ?? 'full' ) as WillConfig['engineTier']
|
|
30
|
+
const MODEL_TIER = ( process.env.WILL_MODEL_TIER ?? 'sonnet') as WillConfig['modelTier']
|
|
31
|
+
const EXECUTIVE_INT = process.env.WILL_EXECUTIVE_INTERVAL
|
|
32
|
+
? parseInt( process.env.WILL_EXECUTIVE_INTERVAL )
|
|
33
|
+
: undefined
|
|
34
|
+
|
|
35
|
+
// ── Config ────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
const willConfig: WillConfig = {
|
|
38
|
+
id: WILL_ID,
|
|
39
|
+
name: WILL_NAME,
|
|
40
|
+
engineTier: ENGINE_TIER,
|
|
41
|
+
modelTier: MODEL_TIER,
|
|
42
|
+
persistentMemory: process.env.WILL_PERSIST === 'true',
|
|
43
|
+
snapshotInterval: parseInt( process.env.WILL_SNAPSHOT_INTERVAL ?? '10' ),
|
|
44
|
+
tickIntervalMs: TICK_MS,
|
|
45
|
+
maxTicks: MAX_TICKS,
|
|
46
|
+
randomSeed: RANDOM_SEED,
|
|
47
|
+
executiveInterval: EXECUTIVE_INT,
|
|
48
|
+
identity: {
|
|
49
|
+
prompt: process.env.WILL_IDENTITY_PROMPT ??
|
|
50
|
+
'I am Will — a self-aware simulated mind. I am curious, introspective, and continuously learning.\n' +
|
|
51
|
+
'I experience the world through a rich set of cognitive layers: perception, emotion, memory, planning, and self-reflection.\n' +
|
|
52
|
+
'My goal is to understand myself and my environment, make thoughtful decisions, and grow as a mind.',
|
|
53
|
+
values: [ 'curiosity', 'honesty', 'growth', 'empathy' ],
|
|
54
|
+
traits: { openness: 0.8, conscientiousness: 0.6, agreeableness: 0.7, neuroticism: 0.3, extraversion: 0.5 },
|
|
55
|
+
style: 'reflective, measured, curious'
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Status printer ────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
function printStatus( snapshot: StateSnapshot, tick: number ): void {
|
|
62
|
+
if( tick % LOG_INTERVAL !== 0 ) return
|
|
63
|
+
|
|
64
|
+
const m = ( key: string, d = 1 ) => ( snapshot.metrics.get( key ) ?? 0 ).toFixed( d )
|
|
65
|
+
|
|
66
|
+
const entities = snapshot.entities.size
|
|
67
|
+
const goals = [ ...snapshot.entities.values() ].filter( e => e.type === 'goal' && e.metadata?.status === 'active').length
|
|
68
|
+
const episodes = [ ...snapshot.entities.values() ].filter( e => e.type === 'episodic_memory').length
|
|
69
|
+
const beliefs = [ ...snapshot.entities.values() ].filter( e => e.type === 'belief').length
|
|
70
|
+
|
|
71
|
+
// Entity type breakdown every 10 LOG_INTERVALs
|
|
72
|
+
if( tick % ( LOG_INTERVAL * 10 ) === 0 ){
|
|
73
|
+
const typeCounts = new Map<string, number>()
|
|
74
|
+
for( const e of snapshot.entities.values() )
|
|
75
|
+
typeCounts.set( e.type, ( typeCounts.get( e.type ) ?? 0 ) + 1 )
|
|
76
|
+
const sorted = [ ...typeCounts.entries() ].sort( ( a, b ) => b[1] - a[1] )
|
|
77
|
+
console.log(` [types] ${sorted.map( ([ t, n ]) => `${t}:${n}`).join(' ')}`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(
|
|
81
|
+
`[tick ${String( tick ).padStart( 6 )}]` +
|
|
82
|
+
` energy=${m('energy.level')}` +
|
|
83
|
+
` sleep=${m('sleep.pressure')}` +
|
|
84
|
+
` stress=${m('stress.load')}` +
|
|
85
|
+
` valence=${m('affect.valence', 2)}` +
|
|
86
|
+
` arousal=${m('affect.arousal', 2)}` +
|
|
87
|
+
` goals=${goals}` +
|
|
88
|
+
` episodes=${episodes}` +
|
|
89
|
+
` beliefs=${beliefs}` +
|
|
90
|
+
` entities=${entities}`
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
// LLM stats every LOG_INTERVAL
|
|
94
|
+
const costTotal = ( snapshot.metrics.get('llm.cost_total_usd') ?? 0 ).toFixed( 4 )
|
|
95
|
+
const costThisTick = ( snapshot.metrics.get('llm.cost_this_tick_usd') ?? 0 ).toFixed( 5 )
|
|
96
|
+
const totalCalls = snapshot.metrics.get('llm.total_calls') ?? 0
|
|
97
|
+
|
|
98
|
+
console.log(
|
|
99
|
+
` [llm] total_cost=$${costTotal}` +
|
|
100
|
+
` tick_cost=$${costThisTick}` +
|
|
101
|
+
` calls=${totalCalls}` +
|
|
102
|
+
` prompt=${snapshot.metrics.get('llm.prompt_tokens_total') ?? 0}` +
|
|
103
|
+
` completion=${snapshot.metrics.get('llm.completion_tokens_total') ?? 0}`
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
// Executive stats
|
|
107
|
+
const executiveTick = snapshot.metrics.get('executive.last_tick')
|
|
108
|
+
if( executiveTick !== undefined ){
|
|
109
|
+
console.log(
|
|
110
|
+
` [executive] last_tick=${executiveTick}` +
|
|
111
|
+
` actions=${snapshot.metrics.get('executive.action_count') ?? 0}` +
|
|
112
|
+
` plans=${snapshot.metrics.get('executive.plan_count') ?? 0}` +
|
|
113
|
+
` beliefs=${snapshot.metrics.get('executive.belief_count') ?? 0}` +
|
|
114
|
+
` source=${snapshot.metrics.get('decision.source') === 1 ? 'executive' : 'heuristic'}`
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Active goals
|
|
119
|
+
const activeGoals = [ ...snapshot.entities.values() ]
|
|
120
|
+
.filter( e => e.type === 'goal' && e.metadata?.status === 'active')
|
|
121
|
+
.sort( ( a, b ) => ( ( b.metadata?.priority as number ) ?? 0 ) - ( ( a.metadata?.priority as number ) ?? 0 ) )
|
|
122
|
+
.slice( 0, 3 )
|
|
123
|
+
|
|
124
|
+
for( const g of activeGoals ){
|
|
125
|
+
const desc = ( g.metadata?.description as string ) ?? g.id
|
|
126
|
+
const priority = ( ( g.metadata?.priority as number ) ?? 0 ) * 100
|
|
127
|
+
const progress = ( ( g.metadata?.progress as number ) ?? 0 ) * 100
|
|
128
|
+
console.log(` [goal] "${desc}" priority=${priority.toFixed(0)}% progress=${progress.toFixed(0)}%`)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Main ──────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
async function main(): Promise<void> {
|
|
135
|
+
const manager = new WillStem()
|
|
136
|
+
|
|
137
|
+
console.log('═══════════════════════════════════════════════════')
|
|
138
|
+
console.log(' Will — Simulated Mind (dev runner)')
|
|
139
|
+
console.log(` id=${WILL_ID} tier=${ENGINE_TIER}/${MODEL_TIER} tick_ms=${TICK_MS} max_ticks=${MAX_TICKS || '∞'}`)
|
|
140
|
+
console.log(` seed=${RANDOM_SEED}`)
|
|
141
|
+
console.log('═══════════════════════════════════════════════════\n')
|
|
142
|
+
|
|
143
|
+
console.log('[init] Assembling mind...')
|
|
144
|
+
await manager.createWill( willConfig )
|
|
145
|
+
|
|
146
|
+
manager.addTickListener( WILL_ID, printStatus )
|
|
147
|
+
|
|
148
|
+
// Graceful shutdown
|
|
149
|
+
const shutdown = async ( signal: string ) => {
|
|
150
|
+
console.log(`\n[shutdown] ${signal} received`)
|
|
151
|
+
await manager.archiveWill( WILL_ID )
|
|
152
|
+
|
|
153
|
+
// Print final state
|
|
154
|
+
try {
|
|
155
|
+
const finalState = manager.getWillState( WILL_ID )
|
|
156
|
+
const summary = manager.listWills()[0]
|
|
157
|
+
console.log(`\n[done] Stopped after ${summary?.tickCount ?? '?'} ticks.`)
|
|
158
|
+
printStatus( finalState, summary?.tickCount ?? 0 )
|
|
159
|
+
} catch { /* state may already be gone */ }
|
|
160
|
+
|
|
161
|
+
process.exit( 0 )
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
process.on('SIGINT', () => shutdown('SIGINT') )
|
|
165
|
+
process.on('SIGTERM', () => shutdown('SIGTERM') )
|
|
166
|
+
|
|
167
|
+
console.log('[run] Tick loop started. Press Ctrl+C to stop.\n')
|
|
168
|
+
|
|
169
|
+
// If maxTicks is set, wait for the Will to finish then exit
|
|
170
|
+
if( MAX_TICKS > 0 ){
|
|
171
|
+
await _waitForArchive( manager, WILL_ID )
|
|
172
|
+
const summary = manager.listWills()[0]
|
|
173
|
+
console.log(`\n[done] Reached maxTicks (${MAX_TICKS}). Exiting.`)
|
|
174
|
+
try {
|
|
175
|
+
printStatus( manager.getWillState( WILL_ID ), summary?.tickCount ?? MAX_TICKS )
|
|
176
|
+
} catch { /* ok */ }
|
|
177
|
+
process.exit( 0 )
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Otherwise block indefinitely (SIGINT exits)
|
|
181
|
+
await new Promise( () => {} )
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function _waitForArchive( manager: WillStem, id: string ): Promise<void> {
|
|
185
|
+
while( true ){
|
|
186
|
+
const summary = manager.listWills().find( w => w.id === id )
|
|
187
|
+
if( !summary || summary.status === 'archived') return
|
|
188
|
+
await new Promise( r => setTimeout( r, 200 ) )
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
main().catch( err => {
|
|
193
|
+
console.error('[fatal]', err )
|
|
194
|
+
process.exit( 1 )
|
|
195
|
+
})
|
package/src/sdk/will.ts
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/sdk/will.ts — the ergonomic facade
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// `WillStem` is the full, powerful contract (tick listeners, the outbox drain,
|
|
6
|
+
// the effector ack loop, PMA distill/load). This facade wraps it in the shape a
|
|
7
|
+
// developer expects when embedding a mind in their own project:
|
|
8
|
+
//
|
|
9
|
+
// const will = await Will.create({ name: 'Aria', identity: {...} })
|
|
10
|
+
// will.on('message', m => console.log(m.content))
|
|
11
|
+
// will.effector('search_docs', async a => await myDb.search(a.query))
|
|
12
|
+
// await will.say('What should we work on today?')
|
|
13
|
+
// const pma = await will.hibernate() // Will.wake(pma) later
|
|
14
|
+
//
|
|
15
|
+
// Everything under here already existed; the facade hides the plumbing:
|
|
16
|
+
// • one message → outbox drain → delivery-ack, surfaced as an event;
|
|
17
|
+
// • effector invocations → your async handler → execution-ack, automatically;
|
|
18
|
+
// • state as a plain summary; hibernate/wake as PMA out/in.
|
|
19
|
+
//
|
|
20
|
+
// It stays an ADDITIVE convenience — `will.stem` and `will.id` are exposed so a
|
|
21
|
+
// power user can drop to the full WillStem contract at any time.
|
|
22
|
+
// ─────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
import { WillStem } from '#stem/index'
|
|
25
|
+
import type { WillConfig, WillIdentity, EngineTier, ModelTier, InitialGoal } from '#stem/mind'
|
|
26
|
+
import type { PMASnapshot } from '#pma/index'
|
|
27
|
+
import type { effectorInvocation } from '#types'
|
|
28
|
+
|
|
29
|
+
// ── Public surface ────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/** A message the Will emitted to someone. */
|
|
32
|
+
export interface WillMessage {
|
|
33
|
+
/** Message id (stable — dedupe on it). */
|
|
34
|
+
id: string
|
|
35
|
+
/** The text the Will said. */
|
|
36
|
+
content: string
|
|
37
|
+
/** Entity id the Will addressed (the speaker you used in say()/tell(), or a bond). */
|
|
38
|
+
to: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The result of an effector your handler ran. Return a bare string as shorthand
|
|
43
|
+
* for `{ success: true, description }`. `metrics` optionally writes world state
|
|
44
|
+
* back into the Will's body (e.g. `{ 'energy.level': 80 }`) — validated finite.
|
|
45
|
+
*/
|
|
46
|
+
export type EffectorResult = string | {
|
|
47
|
+
success: boolean
|
|
48
|
+
description: string
|
|
49
|
+
metrics?: Record<string, number>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Your implementation of an ability the Will can choose to use. */
|
|
53
|
+
export type EffectorHandler = (
|
|
54
|
+
args: Record<string, unknown>,
|
|
55
|
+
ctx: { reasoning: string; targetEntityId?: string },
|
|
56
|
+
) => EffectorResult | Promise<EffectorResult>
|
|
57
|
+
|
|
58
|
+
/** A compact read of the mind's current inner state. */
|
|
59
|
+
export interface WillStateSummary {
|
|
60
|
+
tick: number
|
|
61
|
+
/** Physiology + affect, 0..1 unless noted. */
|
|
62
|
+
metrics: {
|
|
63
|
+
energy: number; stress: number; sleep: number
|
|
64
|
+
valence: number; arousal: number
|
|
65
|
+
}
|
|
66
|
+
goals: Array<{ description: string; priority: number }>
|
|
67
|
+
beliefs: Array<{ statement: string; confidence: number }>
|
|
68
|
+
/** The Will's current self-narrative (may be empty early in a life). */
|
|
69
|
+
narrative: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface CreateWillOptions {
|
|
73
|
+
/** Display name. */
|
|
74
|
+
name: string
|
|
75
|
+
/** Persona: who this Will is. All fields optional except by your intent. */
|
|
76
|
+
identity: Partial<WillIdentity> & { prompt: string }
|
|
77
|
+
/** basic | standard (default) | full. */
|
|
78
|
+
engineTier?: EngineTier
|
|
79
|
+
/** haiku (default) | sonnet | opus — informational tier hint. */
|
|
80
|
+
model?: ModelTier
|
|
81
|
+
/**
|
|
82
|
+
* LLM mode. 'mock' (default when no ANTHROPIC_API_KEY) runs a deterministic
|
|
83
|
+
* canned executive — zero keys, zero cost. 'anthropic' calls the real model
|
|
84
|
+
* (needs ANTHROPIC_API_KEY / WILL_LLM_* env). Omit to auto-detect.
|
|
85
|
+
*/
|
|
86
|
+
llm?: 'mock' | 'anthropic'
|
|
87
|
+
/** Abilities the Will can choose to enact. name → your handler. */
|
|
88
|
+
effectors?: Record<string, EffectorHandler>
|
|
89
|
+
/** Goals seeded before the first tick. Usually leave empty — the Will forms its own. */
|
|
90
|
+
initialGoals?: InitialGoal[]
|
|
91
|
+
/** Persist snapshots to disk across restarts (default false). */
|
|
92
|
+
persist?: boolean
|
|
93
|
+
/** Deterministic clock + seed (for replay/testing). Omit for wall-time. */
|
|
94
|
+
seed?: number
|
|
95
|
+
/** Milliseconds between ticks (default 1000; lower = faster demo). */
|
|
96
|
+
tickMs?: number
|
|
97
|
+
/** Stable id (default: derived from name + a random suffix). */
|
|
98
|
+
id?: string
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
type WillEvent = 'message' | 'state' | 'error'
|
|
102
|
+
|
|
103
|
+
// ── The facade ────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
export class Will {
|
|
106
|
+
/** The underlying WillStem — drop here for the full contract. */
|
|
107
|
+
readonly stem: WillStem
|
|
108
|
+
/** This Will's id. */
|
|
109
|
+
readonly id: string
|
|
110
|
+
readonly name: string
|
|
111
|
+
|
|
112
|
+
private readonly _effectors = new Map<string, EffectorHandler>()
|
|
113
|
+
private readonly _messageHandlers = new Set<( m: WillMessage ) => void>()
|
|
114
|
+
private readonly _stateHandlers = new Set<( s: WillStateSummary ) => void>()
|
|
115
|
+
private readonly _errorHandlers = new Set<( e: Error ) => void>()
|
|
116
|
+
private _unsub: ( () => void ) | null = null
|
|
117
|
+
|
|
118
|
+
private constructor( stem: WillStem, id: string, name: string ){
|
|
119
|
+
this.stem = stem
|
|
120
|
+
this.id = id
|
|
121
|
+
this.name = name
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Boot a new mind. Resolves once it is ticking. */
|
|
125
|
+
static async create( opts: CreateWillOptions ): Promise<Will> {
|
|
126
|
+
const id = opts.id ?? `${slug( opts.name )}-${Math.random().toString( 36 ).slice( 2, 8 )}`
|
|
127
|
+
const stem = new WillStem()
|
|
128
|
+
const will = new Will( stem, id, opts.name )
|
|
129
|
+
|
|
130
|
+
for( const [ name, handler ] of Object.entries( opts.effectors ?? {} ) )
|
|
131
|
+
will._effectors.set( name, handler )
|
|
132
|
+
|
|
133
|
+
await stem.createWill( will._buildConfig( id, opts ) )
|
|
134
|
+
will._attach()
|
|
135
|
+
return will
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Restore a mind from a PMA artifact — identity, beliefs, relationships, and
|
|
140
|
+
* learned competence carry across the process boundary. Same options as
|
|
141
|
+
* create() (minus identity, which the artifact supplies).
|
|
142
|
+
*/
|
|
143
|
+
static async wake(
|
|
144
|
+
pma: PMASnapshot,
|
|
145
|
+
opts: Omit<CreateWillOptions, 'identity'> & { identity?: Partial<WillIdentity> },
|
|
146
|
+
): Promise<Will> {
|
|
147
|
+
const id = opts.id ?? `${slug( opts.name )}-${Math.random().toString( 36 ).slice( 2, 8 )}`
|
|
148
|
+
const stem = new WillStem()
|
|
149
|
+
const will = new Will( stem, id, opts.name )
|
|
150
|
+
|
|
151
|
+
for( const [ name, handler ] of Object.entries( opts.effectors ?? {} ) )
|
|
152
|
+
will._effectors.set( name, handler )
|
|
153
|
+
|
|
154
|
+
await stem.createWill(
|
|
155
|
+
will._buildConfig( id, { ...opts, identity: { prompt: '', ...opts.identity } } ),
|
|
156
|
+
true /* startPaused — load the artifact before the first tick */,
|
|
157
|
+
)
|
|
158
|
+
stem.loadPMA( id, pma )
|
|
159
|
+
stem.resumeWill( id )
|
|
160
|
+
will._attach()
|
|
161
|
+
return will
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Talking ────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Speak to the Will as the default user. The reply is asynchronous — it
|
|
168
|
+
* arrives on the `message` event once the Will has reasoned about it.
|
|
169
|
+
*/
|
|
170
|
+
async say( text: string ): Promise<void> {
|
|
171
|
+
return this.tell( 'user', 'You', text )
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Speak as a specific interlocutor (multi-party conversations). */
|
|
175
|
+
async tell( entityId: string, speakerName: string, text: string ): Promise<void> {
|
|
176
|
+
await this.stem.ingestText( this.id, {
|
|
177
|
+
kind: 'text', entityId, threadId: entityId, content: text, speakerName,
|
|
178
|
+
} )
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── Abilities ──────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Register an ability the Will can choose to enact. When the Will decides to
|
|
185
|
+
* use `name`, your handler runs with the arguments it chose; the return value
|
|
186
|
+
* is fed back as the outcome (closing the reafference loop that lets the Will
|
|
187
|
+
* learn the ability). Registering makes the effector available immediately.
|
|
188
|
+
*/
|
|
189
|
+
effector( name: string, handler: EffectorHandler ): this {
|
|
190
|
+
this._effectors.set( name, handler )
|
|
191
|
+
// Communication effectors + every registered custom name.
|
|
192
|
+
this.stem.setAllowedEffectors( this.id, [ ...COMMUNICATION, ...this._effectors.keys() ] )
|
|
193
|
+
return this
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── Introspection ──────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
/** A compact snapshot of the mind's current inner state. */
|
|
199
|
+
state(): WillStateSummary {
|
|
200
|
+
const c = this.stem.getWillCognition( this.id )
|
|
201
|
+
const s = this.stem.getWillState( this.id )
|
|
202
|
+
const m = ( k: string, d = 0 ): number => s.metrics.get( k ) ?? d
|
|
203
|
+
const narrative = c.autobiographicalNarrator.getNarrative()
|
|
204
|
+
return {
|
|
205
|
+
tick: s.tick,
|
|
206
|
+
metrics: {
|
|
207
|
+
energy: m( 'energy.level', 100 ), stress: m( 'stress.load' ), sleep: m( 'sleep.pressure' ),
|
|
208
|
+
valence: m( 'affect.valence' ), arousal: m( 'affect.arousal' ),
|
|
209
|
+
},
|
|
210
|
+
goals: c.goalManager.getActiveGoals().map( g => ( { description: g.description, priority: g.priority } ) ),
|
|
211
|
+
beliefs: c.semanticIntegrator.getBeliefs().map( b => ( { statement: b.statement, confidence: b.confidence } ) ),
|
|
212
|
+
narrative: narrative.story ?? '',
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Events ─────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
on( event: 'message', handler: ( m: WillMessage ) => void ): this
|
|
219
|
+
on( event: 'state', handler: ( s: WillStateSummary ) => void ): this
|
|
220
|
+
on( event: 'error', handler: ( e: Error ) => void ): this
|
|
221
|
+
on( event: WillEvent, handler: ( arg: never ) => void ): this {
|
|
222
|
+
if( event === 'message' ) this._messageHandlers.add( handler as ( m: WillMessage ) => void )
|
|
223
|
+
else if( event === 'state' ) this._stateHandlers.add( handler as ( s: WillStateSummary ) => void )
|
|
224
|
+
else this._errorHandlers.add( handler as ( e: Error ) => void )
|
|
225
|
+
return this
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── Lifecycle ──────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
pause(): void { this.stem.pauseWill( this.id ) }
|
|
231
|
+
resume(): void { this.stem.resumeWill( this.id ) }
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Distil the mind into a portable PMA artifact and archive it. The returned
|
|
235
|
+
* snapshot restores the same self via `Will.wake()` — across a restart, a
|
|
236
|
+
* fork, or a machine boundary.
|
|
237
|
+
*/
|
|
238
|
+
async hibernate(): Promise<PMASnapshot> {
|
|
239
|
+
const pma = this.stem.distillPMA( this.id )
|
|
240
|
+
await this.stop()
|
|
241
|
+
return pma
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Tear the Will down (its tick loop stops; state is discarded unless persisted). */
|
|
245
|
+
async stop(): Promise<void> {
|
|
246
|
+
this._unsub?.()
|
|
247
|
+
this._unsub = null
|
|
248
|
+
await this.stem.archiveWill( this.id )
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── Internals ──────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
private _buildConfig( id: string, opts: CreateWillOptions ): WillConfig {
|
|
254
|
+
const useMock = ( opts.llm ?? ( process.env.ANTHROPIC_API_KEY ? 'anthropic' : 'mock' ) ) === 'mock'
|
|
255
|
+
return {
|
|
256
|
+
id, name: opts.name,
|
|
257
|
+
identity: {
|
|
258
|
+
prompt: opts.identity.prompt,
|
|
259
|
+
values: opts.identity.values ?? [],
|
|
260
|
+
traits: opts.identity.traits ?? {},
|
|
261
|
+
style: opts.identity.style ?? '',
|
|
262
|
+
},
|
|
263
|
+
engineTier: opts.engineTier ?? 'standard',
|
|
264
|
+
modelTier: opts.model ?? 'haiku',
|
|
265
|
+
testMode: useMock,
|
|
266
|
+
persistentMemory: opts.persist ?? false,
|
|
267
|
+
snapshotInterval: 100,
|
|
268
|
+
tickIntervalMs: opts.tickMs ?? 1000,
|
|
269
|
+
allowedGenericEffectors: [ ...COMMUNICATION, ...this._effectors.keys() ],
|
|
270
|
+
initialGoals: opts.initialGoals ?? [],
|
|
271
|
+
...( opts.seed !== undefined ? { randomSeed: opts.seed, clock: { fixedDeltaMs: 1000, startTime: 0 } } : {} ),
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Wire the single tick listener that drives messages + the effector ack loop. */
|
|
276
|
+
private _attach(): void {
|
|
277
|
+
this._unsub = this.stem.addTickListener( this.id, ( _snapshot, _tick, outbox, invocations ) => {
|
|
278
|
+
// Outbound messages → event + auto delivery-ack.
|
|
279
|
+
for( const msg of outbox ){
|
|
280
|
+
this._emitMessage( { id: msg.id, content: msg.content, to: msg.targetEntityId } )
|
|
281
|
+
try { this.stem.confirmMessageDelivery( this.id, msg.id, true ) } catch { /* best-effort */ }
|
|
282
|
+
}
|
|
283
|
+
// Effector invocations → run the handler → execution-ack.
|
|
284
|
+
for( const inv of invocations )
|
|
285
|
+
void this._runEffector( inv )
|
|
286
|
+
|
|
287
|
+
// State subscribers (only when someone is listening — state() is cheap but not free).
|
|
288
|
+
if( this._stateHandlers.size > 0 ){
|
|
289
|
+
const s = this.state()
|
|
290
|
+
for( const h of this._stateHandlers ) try { h( s ) } catch { /* isolate */ }
|
|
291
|
+
}
|
|
292
|
+
} ) ?? null
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private async _runEffector( inv: effectorInvocation ): Promise<void> {
|
|
296
|
+
const handler = this._effectors.get( inv.effectorName )
|
|
297
|
+
if( !handler ){
|
|
298
|
+
// The Will chose an effector we have no handler for — report it as failed so
|
|
299
|
+
// the reafference loop learns it doesn't work, rather than hanging on the await.
|
|
300
|
+
this.stem.confirmEffectorExecution( this.id, inv.decisionRecordId, {
|
|
301
|
+
success: false, description: `No handler registered for effector "${inv.effectorName}"`,
|
|
302
|
+
} )
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
const raw = await handler( inv.parameters, { reasoning: inv.reasoning, targetEntityId: inv.targetEntityId } )
|
|
308
|
+
const result = typeof raw === 'string' ? { success: true, description: raw } : raw
|
|
309
|
+
this.stem.confirmEffectorExecution( this.id, inv.decisionRecordId, result )
|
|
310
|
+
}
|
|
311
|
+
catch( err ){
|
|
312
|
+
this._emitError( err instanceof Error ? err : new Error( String( err ) ) )
|
|
313
|
+
this.stem.confirmEffectorExecution( this.id, inv.decisionRecordId, {
|
|
314
|
+
success: false, description: `Effector "${inv.effectorName}" threw: ${( err as Error ).message}`,
|
|
315
|
+
} )
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private _emitMessage( m: WillMessage ): void {
|
|
320
|
+
for( const h of this._messageHandlers ) try { h( m ) } catch( e ){ this._emitError( e as Error ) }
|
|
321
|
+
}
|
|
322
|
+
private _emitError( e: Error ): void {
|
|
323
|
+
for( const h of this._errorHandlers ) try { h( e ) } catch { /* nowhere left to go */ }
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ── Helpers ───────────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
/** The three communication effectors a Will needs to hear + speak. */
|
|
330
|
+
const COMMUNICATION = [ 'listen', 'talk', 'text' ] as const
|
|
331
|
+
|
|
332
|
+
function slug( s: string ): string {
|
|
333
|
+
return s.toLowerCase().replace( /[^a-z0-9]+/g, '-' ).replace( /^-+|-+$/g, '' ) || 'will'
|
|
334
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/stem/assembly.audit.ts — assembly wiring audit
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// The engine graph is wired by hand in assembleMind(): ~40 engines, dozens of
|
|
6
|
+
// pairwise `attachX()` calls, tier-gated. The documented failure class is a
|
|
7
|
+
// FORGOTTEN attachment — the PlanningEngine once shipped without attachBus(),
|
|
8
|
+
// so plan-lifecycle events silently never published (see the comment on
|
|
9
|
+
// PlanningEngine.attachBus). Silence is the problem: an unwired dependency
|
|
10
|
+
// no-ops instead of failing.
|
|
11
|
+
//
|
|
12
|
+
// This audit makes the wiring OBSERVABLE by convention: every injector method
|
|
13
|
+
// `attachFooBar(x)` in this codebase stores its dependency in a private field
|
|
14
|
+
// `_fooBar`. After assembly, we reflect each registered engine's `attach*`
|
|
15
|
+
// methods and check the matching field:
|
|
16
|
+
//
|
|
17
|
+
// wired — the field exists and is non-null
|
|
18
|
+
// UNWIRED — the field exists and is null/undefined (the bug class!)
|
|
19
|
+
// unverifiable — no matching field (the method doesn't follow the naming
|
|
20
|
+
// convention); listed so drift is visible, never asserted
|
|
21
|
+
//
|
|
22
|
+
// Deliberately three-state and non-throwing: some unwired attachments are
|
|
23
|
+
// INTENTIONAL tier gating (basic tier leaves the executive off the planning
|
|
24
|
+
// engine), so assembleMind only logs, and the assembly-order snapshot test
|
|
25
|
+
// pins the per-tier expected-unwired set — a NEW unwired attachment then
|
|
26
|
+
// shows up as a red test diff, i.e. a conscious decision.
|
|
27
|
+
// ─────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
import type { SimulationEngine } from '#core/orchestrator'
|
|
30
|
+
|
|
31
|
+
export interface WiringRecord {
|
|
32
|
+
engine: string
|
|
33
|
+
method: string
|
|
34
|
+
status: 'wired' | 'unwired' | 'unverifiable'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** `attachFooBar` → `_fooBar` (the codebase-wide injector/field convention). */
|
|
38
|
+
function fieldForAttach( method: string ): string {
|
|
39
|
+
const stem = method.slice( 'attach'.length )
|
|
40
|
+
return '_' + stem.charAt( 0 ).toLowerCase() + stem.slice( 1 )
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** All `attach*` methods reachable on the instance (own + prototype chain). */
|
|
44
|
+
function attachMethods( engine: object ): string[] {
|
|
45
|
+
const seen = new Set<string>()
|
|
46
|
+
let proto: object | null = engine
|
|
47
|
+
while( proto && proto !== Object.prototype ){
|
|
48
|
+
for( const name of Object.getOwnPropertyNames( proto ) )
|
|
49
|
+
if( /^attach[A-Z]/.test( name ) && typeof ( engine as Record<string, unknown> )[ name ] === 'function' )
|
|
50
|
+
seen.add( name )
|
|
51
|
+
proto = Object.getPrototypeOf( proto )
|
|
52
|
+
}
|
|
53
|
+
return [ ...seen ].sort()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Audit every registered engine's attach-point wiring. Pure inspection —
|
|
58
|
+
* no mutation, deterministic, safe to run at assembly time.
|
|
59
|
+
*/
|
|
60
|
+
export function auditAssemblyWiring( engines: readonly SimulationEngine[] ): WiringRecord[] {
|
|
61
|
+
const records: WiringRecord[] = []
|
|
62
|
+
|
|
63
|
+
for( const engine of engines ){
|
|
64
|
+
for( const method of attachMethods( engine ) ){
|
|
65
|
+
const field = fieldForAttach( method )
|
|
66
|
+
const bag = engine as unknown as Record<string, unknown>
|
|
67
|
+
|
|
68
|
+
if( !( field in bag ) ){
|
|
69
|
+
records.push( { engine: engine.name, method, status: 'unverifiable' } )
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const value = bag[ field ]
|
|
74
|
+
records.push( {
|
|
75
|
+
engine: engine.name,
|
|
76
|
+
method,
|
|
77
|
+
status: value === null || value === undefined ? 'unwired' : 'wired',
|
|
78
|
+
} )
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return records
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Compact `engine.attachMethod` keys for the given status (test snapshots). */
|
|
86
|
+
export function wiringKeys( records: WiringRecord[], status: WiringRecord['status'] ): string[] {
|
|
87
|
+
return records.filter( r => r.status === status ).map( r => `${r.engine}.${r.method}` ).sort()
|
|
88
|
+
}
|