@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.
- package/README.md +155 -0
- package/VEIL_CORE_SPEC_v1.md +364 -0
- package/package.json +36 -0
- package/server.ts +302 -0
- package/veil-runtime.ts +414 -0
- package/veil-sdk.ts +311 -0
package/veil-runtime.ts
ADDED
|
@@ -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
|
+
}
|