@swanlabs/veil-core 1.0.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.
@@ -0,0 +1,414 @@
1
+ /**
2
+ * VEIL CORE Runtime v1.0.0
3
+ * Swan Labs — Ambient Web Entity Engine
4
+ *
5
+ * The server-side soul. Runs 24/7. Evolves on its own clock.
6
+ * Infrastructure-agnostic. Runs on any Node.js host.
7
+ */
8
+
9
+ import crypto from "crypto";
10
+
11
+ // ─── Types ───────────────────────────────────────────────────────────────────
12
+
13
+ export interface EntityAxes {
14
+ trust: number;
15
+ density: number;
16
+ warmth: number;
17
+ tension: number;
18
+ memory_depth: number;
19
+ [key: string]: number;
20
+ }
21
+
22
+ export interface PhaseTransition {
23
+ from: number;
24
+ to: number;
25
+ timestamp: number;
26
+ trigger: string;
27
+ }
28
+
29
+ export interface RelationshipEvent {
30
+ type: "visit" | "dwell" | "interaction" | "phase_transition" | "custom";
31
+ timestamp: number;
32
+ data: Record<string, unknown>;
33
+ }
34
+
35
+ export interface VEILRelationship {
36
+ id: string;
37
+ entity_id: string;
38
+ vid: string;
39
+ first_contact: number;
40
+ last_contact: number;
41
+ visit_count: number;
42
+ total_dwell_seconds: number;
43
+ axes: EntityAxes;
44
+ phase: number;
45
+ phase_transitions: PhaseTransition[];
46
+ events: RelationshipEvent[];
47
+ }
48
+
49
+ export interface VEILEntity {
50
+ id: string;
51
+ name: string;
52
+ version: string;
53
+ created_at: number;
54
+ clock_age: number;
55
+ last_ticked: number;
56
+ tick_interval: number;
57
+ axes: EntityAxes;
58
+ phase: number;
59
+ phase_history: PhaseTransition[];
60
+ visitor_count: number;
61
+ relationship_count: number;
62
+ state_hash: string;
63
+ signed_at: number;
64
+ }
65
+
66
+ export interface VEILMorphology {
67
+ visual_density: number;
68
+ color_temperature: number;
69
+ motion_speed: number;
70
+ presence_radius: number;
71
+ revelation: number;
72
+ phase_label: string;
73
+ can_initiate: boolean;
74
+ can_fuse: boolean;
75
+ }
76
+
77
+ // ─── Constants ───────────────────────────────────────────────────────────────
78
+
79
+ const PHASE_NAMES = ["DORMANT", "AWARE", "FAMILIAR", "KNOWN", "FUSED"];
80
+
81
+ const PHASE_RULES = [
82
+ { phase: 0, label: "DORMANT", requires: () => true },
83
+ { phase: 1, label: "AWARE", requires: (r: VEILRelationship) => r.visit_count >= 1 },
84
+ { phase: 2, label: "FAMILIAR", requires: (r: VEILRelationship) => r.visit_count >= 3 && r.axes.trust >= 0.25 },
85
+ { phase: 3, label: "KNOWN", requires: (r: VEILRelationship) => r.visit_count >= 5 && r.axes.trust >= 0.50 },
86
+ { phase: 4, label: "FUSED", requires: (r: VEILRelationship) => r.visit_count >= 8 && r.axes.trust >= 0.75 && r.total_dwell_seconds >= 1800 },
87
+ ];
88
+
89
+ const DEFAULT_AXES: EntityAxes = {
90
+ trust: 0,
91
+ density: 0.2,
92
+ warmth: 0.4,
93
+ tension: 0.35,
94
+ memory_depth: 0.1,
95
+ };
96
+
97
+ const TICK_INTERVAL_MS = 60_000; // 1 minute
98
+
99
+ // ─── ID Generation ───────────────────────────────────────────────────────────
100
+
101
+ export function generateEntityId(): string {
102
+ return crypto.randomUUID();
103
+ }
104
+
105
+ /**
106
+ * Derives a VEIL ID from behavioral fingerprint inputs.
107
+ * One-way — cannot be reversed to recover inputs.
108
+ */
109
+ export function deriveVID(fingerprintInputs: string[]): string {
110
+ const canonical = [...fingerprintInputs].sort().join("|");
111
+ const first = crypto.createHash("sha256").update(canonical).digest("hex");
112
+ const second = crypto.createHash("sha256").update(first).digest("hex");
113
+ // Base58-like encoding (simplified — production would use full base58)
114
+ return "vid_" + second.slice(0, 32);
115
+ }
116
+
117
+ // ─── Entity Factory ───────────────────────────────────────────────────────────
118
+
119
+ export function createEntity(name: string, customAxes?: Partial<EntityAxes>): VEILEntity {
120
+ const now = Date.now();
121
+ const axes = { ...DEFAULT_AXES, ...customAxes };
122
+ const entity: VEILEntity = {
123
+ id: generateEntityId(),
124
+ name,
125
+ version: "1.0.0",
126
+ created_at: now,
127
+ clock_age: 0,
128
+ last_ticked: now,
129
+ tick_interval: TICK_INTERVAL_MS,
130
+ axes,
131
+ phase: 0,
132
+ phase_history: [],
133
+ visitor_count: 0,
134
+ relationship_count: 0,
135
+ state_hash: "",
136
+ signed_at: now,
137
+ };
138
+ entity.state_hash = hashEntityState(entity);
139
+ return entity;
140
+ }
141
+
142
+ // ─── Relationship Factory ─────────────────────────────────────────────────────
143
+
144
+ export function createRelationship(entityId: string, vid: string): VEILRelationship {
145
+ return {
146
+ id: crypto.randomUUID(),
147
+ entity_id: entityId,
148
+ vid,
149
+ first_contact: Date.now(),
150
+ last_contact: Date.now(),
151
+ visit_count: 0,
152
+ total_dwell_seconds: 0,
153
+ axes: { ...DEFAULT_AXES },
154
+ phase: 0,
155
+ phase_transitions: [],
156
+ events: [],
157
+ };
158
+ }
159
+
160
+ // ─── Phase Engine ─────────────────────────────────────────────────────────────
161
+
162
+ export function computePhase(relationship: VEILRelationship): number {
163
+ let highest = 0;
164
+ for (const rule of PHASE_RULES) {
165
+ if (rule.requires(relationship)) highest = rule.phase;
166
+ }
167
+ return highest;
168
+ }
169
+
170
+ export function applyPhaseTransitions(
171
+ relationship: VEILRelationship
172
+ ): { relationship: VEILRelationship; transitioned: boolean; newPhase: number } {
173
+ const newPhase = computePhase(relationship);
174
+ if (newPhase <= relationship.phase) {
175
+ return { relationship, transitioned: false, newPhase: relationship.phase };
176
+ }
177
+ const transition: PhaseTransition = {
178
+ from: relationship.phase,
179
+ to: newPhase,
180
+ timestamp: Date.now(),
181
+ trigger: "phase_rules",
182
+ };
183
+ return {
184
+ relationship: {
185
+ ...relationship,
186
+ phase: newPhase,
187
+ phase_transitions: [...relationship.phase_transitions, transition],
188
+ events: [
189
+ ...relationship.events,
190
+ { type: "phase_transition", timestamp: Date.now(), data: transition },
191
+ ],
192
+ },
193
+ transitioned: true,
194
+ newPhase,
195
+ };
196
+ }
197
+
198
+ // ─── Visit Processing ─────────────────────────────────────────────────────────
199
+
200
+ export interface VisitResult {
201
+ entity: VEILEntity;
202
+ relationship: VEILRelationship;
203
+ morphology: VEILMorphology;
204
+ is_returning: boolean;
205
+ gap_minutes: number;
206
+ phase_transitioned: boolean;
207
+ new_phase?: number;
208
+ }
209
+
210
+ export function processVisit(
211
+ entity: VEILEntity,
212
+ relationship: VEILRelationship | null,
213
+ vid: string
214
+ ): VisitResult {
215
+ const now = Date.now();
216
+ const isReturning = relationship !== null;
217
+ let rel = relationship ?? createRelationship(entity.id, vid);
218
+
219
+ const gapMinutes = isReturning
220
+ ? (now - rel.last_contact) / 60000
221
+ : 0;
222
+
223
+ // Trust gains from returning
224
+ const trustGain = isReturning
225
+ ? Math.min(0.08 + Math.min(gapMinutes / 1440, 0.04), 0.15)
226
+ : 0.02;
227
+
228
+ // Axis updates on visit
229
+ const updatedAxes: EntityAxes = {
230
+ ...rel.axes,
231
+ trust: clamp(rel.axes.trust + trustGain),
232
+ density: clamp(rel.axes.density + 0.03),
233
+ warmth: clamp(rel.axes.warmth + (isReturning ? 0.02 : 0.01)),
234
+ tension: clamp(rel.axes.tension - (gapMinutes > 60 ? 0.05 : 0)),
235
+ memory_depth: clamp(rel.axes.memory_depth + 0.02),
236
+ };
237
+
238
+ rel = {
239
+ ...rel,
240
+ last_contact: now,
241
+ visit_count: rel.visit_count + 1,
242
+ axes: updatedAxes,
243
+ events: [
244
+ ...rel.events,
245
+ {
246
+ type: "visit",
247
+ timestamp: now,
248
+ data: { visit_count: rel.visit_count + 1, gap_minutes: gapMinutes, is_returning: isReturning },
249
+ },
250
+ ],
251
+ };
252
+
253
+ // Phase check
254
+ const { relationship: phasedRel, transitioned, newPhase } = applyPhaseTransitions(rel);
255
+ rel = phasedRel;
256
+
257
+ // Update entity visitor count
258
+ const updatedEntity: VEILEntity = {
259
+ ...entity,
260
+ visitor_count: isReturning ? entity.visitor_count : entity.visitor_count + 1,
261
+ relationship_count: isReturning ? entity.relationship_count : entity.relationship_count + 1,
262
+ };
263
+ updatedEntity.state_hash = hashEntityState(updatedEntity);
264
+
265
+ const morphology = deriveMorphology(updatedEntity, rel);
266
+
267
+ return {
268
+ entity: updatedEntity,
269
+ relationship: rel,
270
+ morphology,
271
+ is_returning: isReturning,
272
+ gap_minutes: gapMinutes,
273
+ phase_transitioned: transitioned,
274
+ new_phase: transitioned ? newPhase : undefined,
275
+ };
276
+ }
277
+
278
+ // ─── Dwell Recording ─────────────────────────────────────────────────────────
279
+
280
+ export function recordDwell(
281
+ relationship: VEILRelationship,
282
+ dwellSeconds: number
283
+ ): VEILRelationship {
284
+ const trustFromDwell = Math.min(dwellSeconds / 300, 0.1); // Max 0.1 per session
285
+ return {
286
+ ...relationship,
287
+ total_dwell_seconds: relationship.total_dwell_seconds + dwellSeconds,
288
+ axes: {
289
+ ...relationship.axes,
290
+ trust: clamp(relationship.axes.trust + trustFromDwell),
291
+ memory_depth: clamp(relationship.axes.memory_depth + dwellSeconds / 36000),
292
+ },
293
+ events: [
294
+ ...relationship.events,
295
+ {
296
+ type: "dwell",
297
+ timestamp: Date.now(),
298
+ data: { dwell_seconds: dwellSeconds, trust_gained: trustFromDwell },
299
+ },
300
+ ],
301
+ };
302
+ }
303
+
304
+ // ─── Entity Clock ─────────────────────────────────────────────────────────────
305
+
306
+ export interface TickResult {
307
+ entity: VEILEntity;
308
+ relationships: VEILRelationship[];
309
+ axis_deltas: Partial<EntityAxes>;
310
+ ticked_at: number;
311
+ clock_age_minutes: number;
312
+ }
313
+
314
+ /**
315
+ * Advances the entity clock by one tick.
316
+ * Called by the runtime scheduler — NOT by visitor requests.
317
+ * This is what makes the entity alive between visits.
318
+ */
319
+ export function tickEntity(
320
+ entity: VEILEntity,
321
+ relationships: VEILRelationship[]
322
+ ): TickResult {
323
+ const now = Date.now();
324
+ const minutesSinceLastTick = (now - entity.last_ticked) / 60000;
325
+ const newClockAge = entity.clock_age + minutesSinceLastTick;
326
+
327
+ // Entity-level axis drift (not per-visitor)
328
+ const axisDelta: Partial<EntityAxes> = {
329
+ warmth: entity.axes.warmth + (0.5 - entity.axes.warmth) * 0.0001 * minutesSinceLastTick,
330
+ tension: Math.max(0, entity.axes.tension - 0.0002 * minutesSinceLastTick),
331
+ density: Math.min(1, entity.axes.density + 0.00005 * minutesSinceLastTick),
332
+ };
333
+
334
+ const updatedEntity: VEILEntity = {
335
+ ...entity,
336
+ clock_age: newClockAge,
337
+ last_ticked: now,
338
+ axes: {
339
+ ...entity.axes,
340
+ ...Object.fromEntries(
341
+ Object.entries(axisDelta).map(([k, v]) => [k, clamp(v as number)])
342
+ ),
343
+ },
344
+ };
345
+ updatedEntity.state_hash = hashEntityState(updatedEntity);
346
+
347
+ // Per-relationship trust decay for absent visitors
348
+ const updatedRelationships = relationships.map(rel => {
349
+ const minutesAbsent = (now - rel.last_contact) / 60000;
350
+ if (minutesAbsent < 60) return rel; // No decay under 1 hour
351
+
352
+ const dayAbsent = minutesAbsent / 1440;
353
+ const trustDecay = Math.min(dayAbsent * 0.01, 0.3); // Max 0.3 total decay
354
+ const decayedTrust = Math.max(0, rel.axes.trust - trustDecay * (minutesSinceLastTick / 1440));
355
+
356
+ return {
357
+ ...rel,
358
+ axes: { ...rel.axes, trust: decayedTrust },
359
+ };
360
+ });
361
+
362
+ return {
363
+ entity: updatedEntity,
364
+ relationships: updatedRelationships,
365
+ axis_deltas: axisDelta,
366
+ ticked_at: now,
367
+ clock_age_minutes: newClockAge,
368
+ };
369
+ }
370
+
371
+ // ─── Morphology Derivation ────────────────────────────────────────────────────
372
+
373
+ export function deriveMorphology(
374
+ entity: VEILEntity,
375
+ relationship: VEILRelationship
376
+ ): VEILMorphology {
377
+ const axes = relationship.axes;
378
+ return {
379
+ visual_density: axes.density,
380
+ color_temperature: axes.warmth,
381
+ motion_speed: clamp(axes.tension * 0.4 + axes.trust * 0.2 + 0.1),
382
+ presence_radius: clamp(axes.trust * 0.5 + axes.density * 0.3 + 0.1),
383
+ revelation: clamp(axes.trust * 0.6 + axes.memory_depth * 0.4),
384
+ phase_label: PHASE_NAMES[relationship.phase] ?? "UNKNOWN",
385
+ can_initiate: relationship.phase >= 2,
386
+ can_fuse: relationship.phase >= 4,
387
+ };
388
+ }
389
+
390
+ // ─── State Hashing ────────────────────────────────────────────────────────────
391
+
392
+ export function hashEntityState(entity: Omit<VEILEntity, "state_hash" | "signed_at">): string {
393
+ const canonical = JSON.stringify({
394
+ id: entity.id,
395
+ clock_age: Math.round(entity.clock_age * 1000) / 1000,
396
+ axes: entity.axes,
397
+ phase: entity.phase,
398
+ visitor_count: entity.visitor_count,
399
+ last_ticked: entity.last_ticked,
400
+ });
401
+ return crypto.createHash("sha256").update(canonical).digest("hex");
402
+ }
403
+
404
+ // ─── Utility ──────────────────────────────────────────────────────────────────
405
+
406
+ function clamp(v: number, min = 0, max = 1): number {
407
+ return Math.max(min, Math.min(max, v));
408
+ }
409
+
410
+ export function formatClockAge(minutes: number): string {
411
+ if (minutes < 60) return `${Math.floor(minutes)}m`;
412
+ if (minutes < 1440) return `${Math.floor(minutes / 60)}h`;
413
+ return `${Math.floor(minutes / 1440)}d`;
414
+ }