@affectively/aeon-pages 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +625 -0
  3. package/examples/basic/aeon.config.ts +39 -0
  4. package/examples/basic/components/Cursor.tsx +86 -0
  5. package/examples/basic/components/OfflineIndicator.tsx +103 -0
  6. package/examples/basic/components/PresenceBar.tsx +77 -0
  7. package/examples/basic/package.json +20 -0
  8. package/examples/basic/pages/index.tsx +80 -0
  9. package/package.json +101 -0
  10. package/packages/analytics/README.md +309 -0
  11. package/packages/analytics/build.ts +35 -0
  12. package/packages/analytics/package.json +50 -0
  13. package/packages/analytics/src/click-tracker.ts +368 -0
  14. package/packages/analytics/src/context-bridge.ts +319 -0
  15. package/packages/analytics/src/data-layer.ts +302 -0
  16. package/packages/analytics/src/gtm-loader.ts +239 -0
  17. package/packages/analytics/src/index.ts +230 -0
  18. package/packages/analytics/src/merkle-tree.ts +489 -0
  19. package/packages/analytics/src/provider.tsx +300 -0
  20. package/packages/analytics/src/types.ts +320 -0
  21. package/packages/analytics/src/use-analytics.ts +296 -0
  22. package/packages/analytics/tsconfig.json +19 -0
  23. package/packages/benchmarks/src/benchmark.test.ts +691 -0
  24. package/packages/cli/dist/index.js +61899 -0
  25. package/packages/cli/package.json +43 -0
  26. package/packages/cli/src/commands/build.test.ts +682 -0
  27. package/packages/cli/src/commands/build.ts +890 -0
  28. package/packages/cli/src/commands/dev.ts +473 -0
  29. package/packages/cli/src/commands/init.ts +409 -0
  30. package/packages/cli/src/commands/start.ts +297 -0
  31. package/packages/cli/src/index.ts +105 -0
  32. package/packages/directives/src/use-aeon.ts +272 -0
  33. package/packages/mcp-server/package.json +51 -0
  34. package/packages/mcp-server/src/index.ts +178 -0
  35. package/packages/mcp-server/src/resources.ts +346 -0
  36. package/packages/mcp-server/src/tools/index.ts +36 -0
  37. package/packages/mcp-server/src/tools/navigation.ts +545 -0
  38. package/packages/mcp-server/tsconfig.json +21 -0
  39. package/packages/react/package.json +40 -0
  40. package/packages/react/src/Link.tsx +388 -0
  41. package/packages/react/src/components/InstallPrompt.tsx +286 -0
  42. package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
  43. package/packages/react/src/components/PushNotifications.tsx +453 -0
  44. package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
  45. package/packages/react/src/hooks/useConflicts.ts +277 -0
  46. package/packages/react/src/hooks/useNetworkState.ts +209 -0
  47. package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
  48. package/packages/react/src/hooks/useServiceWorker.ts +278 -0
  49. package/packages/react/src/hooks.ts +195 -0
  50. package/packages/react/src/index.ts +151 -0
  51. package/packages/react/src/provider.tsx +467 -0
  52. package/packages/react/tsconfig.json +19 -0
  53. package/packages/runtime/README.md +399 -0
  54. package/packages/runtime/build.ts +48 -0
  55. package/packages/runtime/package.json +71 -0
  56. package/packages/runtime/schema.sql +40 -0
  57. package/packages/runtime/src/api-routes.ts +465 -0
  58. package/packages/runtime/src/benchmark.ts +171 -0
  59. package/packages/runtime/src/cache.ts +479 -0
  60. package/packages/runtime/src/durable-object.ts +1341 -0
  61. package/packages/runtime/src/index.ts +360 -0
  62. package/packages/runtime/src/navigation.test.ts +421 -0
  63. package/packages/runtime/src/navigation.ts +422 -0
  64. package/packages/runtime/src/nextjs-adapter.ts +272 -0
  65. package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
  66. package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
  67. package/packages/runtime/src/offline/encryption.test.ts +412 -0
  68. package/packages/runtime/src/offline/encryption.ts +397 -0
  69. package/packages/runtime/src/offline/types.ts +465 -0
  70. package/packages/runtime/src/predictor.ts +371 -0
  71. package/packages/runtime/src/registry.ts +351 -0
  72. package/packages/runtime/src/router/context-extractor.ts +661 -0
  73. package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
  74. package/packages/runtime/src/router/esi-control.ts +541 -0
  75. package/packages/runtime/src/router/esi-cyrano.ts +779 -0
  76. package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
  77. package/packages/runtime/src/router/esi-react.tsx +1065 -0
  78. package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
  79. package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
  80. package/packages/runtime/src/router/esi-translate.ts +503 -0
  81. package/packages/runtime/src/router/esi.ts +666 -0
  82. package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
  83. package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
  84. package/packages/runtime/src/router/index.ts +298 -0
  85. package/packages/runtime/src/router/merkle-capability.ts +473 -0
  86. package/packages/runtime/src/router/speculation.ts +451 -0
  87. package/packages/runtime/src/router/types.ts +630 -0
  88. package/packages/runtime/src/router.test.ts +470 -0
  89. package/packages/runtime/src/router.ts +302 -0
  90. package/packages/runtime/src/server.ts +481 -0
  91. package/packages/runtime/src/service-worker-push.ts +319 -0
  92. package/packages/runtime/src/service-worker.ts +553 -0
  93. package/packages/runtime/src/skeleton-hydrate.ts +237 -0
  94. package/packages/runtime/src/speculation.test.ts +389 -0
  95. package/packages/runtime/src/speculation.ts +486 -0
  96. package/packages/runtime/src/storage.test.ts +1297 -0
  97. package/packages/runtime/src/storage.ts +1048 -0
  98. package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
  99. package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
  100. package/packages/runtime/src/sync/coordinator.test.ts +608 -0
  101. package/packages/runtime/src/sync/coordinator.ts +596 -0
  102. package/packages/runtime/src/tree-compiler.ts +295 -0
  103. package/packages/runtime/src/types.ts +728 -0
  104. package/packages/runtime/src/worker.ts +327 -0
  105. package/packages/runtime/tsconfig.json +20 -0
  106. package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
  107. package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
  108. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
  109. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
  110. package/packages/runtime/wasm/package.json +21 -0
  111. package/packages/runtime/wrangler.toml +41 -0
  112. package/packages/runtime-wasm/Cargo.lock +436 -0
  113. package/packages/runtime-wasm/Cargo.toml +29 -0
  114. package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
  115. package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
  116. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
  117. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
  118. package/packages/runtime-wasm/pkg/package.json +21 -0
  119. package/packages/runtime-wasm/src/hydrate.rs +352 -0
  120. package/packages/runtime-wasm/src/lib.rs +191 -0
  121. package/packages/runtime-wasm/src/render.rs +629 -0
  122. package/packages/runtime-wasm/src/router.rs +298 -0
  123. package/packages/runtime-wasm/src/skeleton.rs +430 -0
  124. package/rfcs/RFC-001-ZERO-DEPENDENCY-RENDERING.md +1446 -0
@@ -0,0 +1,557 @@
1
+ /**
2
+ * Heuristic Router Adapter
3
+ *
4
+ * Zero-latency personalized routing using pure heuristics.
5
+ * No external API calls - all decisions made locally via WASM-compatible logic.
6
+ *
7
+ * Signals used:
8
+ * - User tier → feature gating
9
+ * - Viewport → responsive layout selection, density
10
+ * - Custom signals → theme/accent derivation (configurable)
11
+ * - Navigation history → component ordering, speculation
12
+ * - Time of day → theme suggestion
13
+ * - Connection speed → prefetch depth
14
+ */
15
+
16
+ import type {
17
+ ComponentNode,
18
+ ComponentTree,
19
+ EmotionState,
20
+ LayoutDensity,
21
+ RouteDecision,
22
+ RouterAdapter,
23
+ SkeletonHints,
24
+ ThemeMode,
25
+ UserContext,
26
+ UserTier,
27
+ } from './types';
28
+
29
+ // ============================================================================
30
+ // Configuration Types
31
+ // ============================================================================
32
+
33
+ /**
34
+ * Feature flags configuration by tier
35
+ */
36
+ export type TierFeatures = Record<UserTier, Record<string, boolean>>;
37
+
38
+ /**
39
+ * Custom signal processor for deriving values from user context
40
+ */
41
+ export interface SignalProcessor {
42
+ /** Derive accent color from context */
43
+ deriveAccent?: (context: UserContext) => string;
44
+
45
+ /** Derive theme from context */
46
+ deriveTheme?: (context: UserContext) => ThemeMode;
47
+
48
+ /** Custom component relevance scoring */
49
+ scoreRelevance?: (node: ComponentNode, context: UserContext) => number;
50
+
51
+ /** Custom navigation prediction */
52
+ predictNavigation?: (currentPath: string, context: UserContext) => string[];
53
+ }
54
+
55
+ /**
56
+ * Heuristic adapter configuration
57
+ */
58
+ export interface HeuristicAdapterConfig {
59
+ /** Feature flags by tier (optional - defaults to all features enabled) */
60
+ tierFeatures?: TierFeatures;
61
+
62
+ /** Default accent color when no signal processor provided */
63
+ defaultAccent?: string;
64
+
65
+ /** Custom signal processing */
66
+ signals?: SignalProcessor;
67
+
68
+ /** Default paths to suggest when no history available */
69
+ defaultPaths?: string[];
70
+
71
+ /** Maximum number of paths to speculate */
72
+ maxSpeculationPaths?: number;
73
+ }
74
+
75
+ // ============================================================================
76
+ // Default Configuration
77
+ // ============================================================================
78
+
79
+ const DEFAULT_CONFIG: Required<HeuristicAdapterConfig> = {
80
+ tierFeatures: {
81
+ free: {},
82
+ starter: {},
83
+ pro: {},
84
+ enterprise: {},
85
+ admin: {}, // Admins get all features
86
+ },
87
+ defaultAccent: '#336699', // Steel blue - neutral default
88
+ signals: {},
89
+ defaultPaths: ['/'],
90
+ maxSpeculationPaths: 5,
91
+ };
92
+
93
+ // ============================================================================
94
+ // Theme Derivation
95
+ // ============================================================================
96
+
97
+ /**
98
+ * Default theme derivation based on time
99
+ */
100
+ function defaultDeriveTheme(context: UserContext): ThemeMode {
101
+ // Explicit preference takes priority
102
+ if (context.preferences.theme) {
103
+ return context.preferences.theme as ThemeMode;
104
+ }
105
+
106
+ // Time-based suggestion
107
+ const hour = context.localHour;
108
+ const isNight = hour >= 20 || hour < 6;
109
+ const isEvening = hour >= 18 && hour < 20;
110
+
111
+ if (isNight) {
112
+ return 'dark';
113
+ }
114
+
115
+ if (isEvening) {
116
+ return 'auto';
117
+ }
118
+
119
+ return 'light';
120
+ }
121
+
122
+ /**
123
+ * Determine layout density based on viewport and preferences
124
+ */
125
+ function determineDensity(context: UserContext): LayoutDensity {
126
+ // Explicit preference
127
+ if (context.preferences.density) {
128
+ return context.preferences.density as LayoutDensity;
129
+ }
130
+
131
+ // Viewport-based
132
+ const { width, height } = context.viewport;
133
+
134
+ // Mobile
135
+ if (width < 768) {
136
+ return 'compact';
137
+ }
138
+
139
+ // Large desktop with plenty of space
140
+ if (width >= 1440 && height >= 900) {
141
+ return 'comfortable';
142
+ }
143
+
144
+ // Default
145
+ return 'normal';
146
+ }
147
+
148
+ // ============================================================================
149
+ // Navigation Prediction (Markov Chain)
150
+ // ============================================================================
151
+
152
+ interface TransitionMatrix {
153
+ [from: string]: { [to: string]: number };
154
+ }
155
+
156
+ /**
157
+ * Build transition matrix from navigation history
158
+ */
159
+ function buildTransitionMatrix(history: string[]): TransitionMatrix {
160
+ const matrix: TransitionMatrix = {};
161
+
162
+ for (let i = 0; i < history.length - 1; i++) {
163
+ const from = history[i];
164
+ const to = history[i + 1];
165
+
166
+ if (!matrix[from]) {
167
+ matrix[from] = {};
168
+ }
169
+ matrix[from][to] = (matrix[from][to] || 0) + 1;
170
+ }
171
+
172
+ // Normalize to probabilities
173
+ for (const from of Object.keys(matrix)) {
174
+ const total = Object.values(matrix[from]).reduce((a, b) => a + b, 0);
175
+ for (const to of Object.keys(matrix[from])) {
176
+ matrix[from][to] /= total;
177
+ }
178
+ }
179
+
180
+ return matrix;
181
+ }
182
+
183
+ /**
184
+ * Predict next routes based on current path and history
185
+ */
186
+ function defaultPredictNavigation(
187
+ currentPath: string,
188
+ context: UserContext,
189
+ defaultPaths: string[],
190
+ topN: number,
191
+ ): string[] {
192
+ const history = context.recentPages;
193
+
194
+ // If we have enough history, use Markov chain
195
+ if (history.length >= 3) {
196
+ const matrix = buildTransitionMatrix(history);
197
+ const transitions = matrix[currentPath];
198
+
199
+ if (transitions) {
200
+ const sorted = Object.entries(transitions)
201
+ .sort(([, a], [, b]) => b - a)
202
+ .slice(0, topN)
203
+ .map(([path]) => path);
204
+
205
+ if (sorted.length > 0) {
206
+ return sorted;
207
+ }
208
+ }
209
+ }
210
+
211
+ // Fallback to default paths (excluding current)
212
+ return defaultPaths.filter((p) => p !== currentPath).slice(0, topN);
213
+ }
214
+
215
+ // ============================================================================
216
+ // Component Relevance Scoring
217
+ // ============================================================================
218
+
219
+ /**
220
+ * Default component relevance scoring
221
+ */
222
+ function defaultScoreRelevance(
223
+ node: ComponentNode,
224
+ context: UserContext,
225
+ ): number {
226
+ let score = 50; // Base score
227
+
228
+ // Tier gating
229
+ if (node.requiredTier) {
230
+ const tierOrder: UserTier[] = ['free', 'starter', 'pro', 'enterprise'];
231
+ const requiredIndex = tierOrder.indexOf(node.requiredTier);
232
+ const userIndex = tierOrder.indexOf(context.tier);
233
+
234
+ if (userIndex < requiredIndex) {
235
+ return 0; // User doesn't have access
236
+ }
237
+ score += 10;
238
+ }
239
+
240
+ // Relevance signals
241
+ if (node.relevanceSignals) {
242
+ for (const signal of node.relevanceSignals) {
243
+ // Recent pages signal
244
+ if (signal.startsWith('recentPage:')) {
245
+ const page = signal.slice('recentPage:'.length);
246
+ if (context.recentPages.includes(page)) {
247
+ score += 20;
248
+ }
249
+ }
250
+
251
+ // Time signal
252
+ if (signal.startsWith('timeOfDay:')) {
253
+ const timeRange = signal.slice('timeOfDay:'.length);
254
+ const hour = context.localHour;
255
+
256
+ if (timeRange === 'morning' && hour >= 5 && hour < 12) score += 15;
257
+ if (timeRange === 'afternoon' && hour >= 12 && hour < 17) score += 15;
258
+ if (timeRange === 'evening' && hour >= 17 && hour < 21) score += 15;
259
+ if (timeRange === 'night' && (hour >= 21 || hour < 5)) score += 15;
260
+ }
261
+
262
+ // Preference signal
263
+ if (signal.startsWith('preference:')) {
264
+ const pref = signal.slice('preference:'.length);
265
+ if (context.preferences[pref]) {
266
+ score += 20;
267
+ }
268
+ }
269
+
270
+ // Tier signal
271
+ if (signal.startsWith('tier:')) {
272
+ const requiredTier = signal.slice('tier:'.length) as UserTier;
273
+ const tierOrder: UserTier[] = ['free', 'starter', 'pro', 'enterprise'];
274
+ if (
275
+ tierOrder.indexOf(context.tier) >= tierOrder.indexOf(requiredTier)
276
+ ) {
277
+ score += 15;
278
+ }
279
+ }
280
+ }
281
+ }
282
+
283
+ // Default hidden penalty
284
+ if (node.defaultHidden) {
285
+ score -= 30;
286
+ }
287
+
288
+ return Math.max(0, Math.min(100, score));
289
+ }
290
+
291
+ /**
292
+ * Order components by relevance
293
+ */
294
+ function orderComponentsByRelevance(
295
+ tree: ComponentTree,
296
+ context: UserContext,
297
+ scoreRelevance: (node: ComponentNode, context: UserContext) => number,
298
+ ): string[] {
299
+ const scored: Array<{ id: string; score: number }> = [];
300
+
301
+ tree.nodes.forEach((node, id) => {
302
+ scored.push({
303
+ id,
304
+ score: scoreRelevance(node, context),
305
+ });
306
+ });
307
+
308
+ return scored.sort((a, b) => b.score - a.score).map((s) => s.id);
309
+ }
310
+
311
+ /**
312
+ * Find components to hide based on tier and relevance
313
+ */
314
+ function findHiddenComponents(
315
+ tree: ComponentTree,
316
+ context: UserContext,
317
+ scoreRelevance: (node: ComponentNode, context: UserContext) => number,
318
+ ): string[] {
319
+ const hidden: string[] = [];
320
+
321
+ tree.nodes.forEach((node, id) => {
322
+ const score = scoreRelevance(node, context);
323
+ if (score === 0) {
324
+ hidden.push(id);
325
+ }
326
+ });
327
+
328
+ return hidden;
329
+ }
330
+
331
+ // ============================================================================
332
+ // Skeleton Hints
333
+ // ============================================================================
334
+
335
+ /**
336
+ * Compute skeleton hints for the route
337
+ */
338
+ function computeSkeletonHints(
339
+ route: string,
340
+ context: UserContext,
341
+ tree: ComponentTree,
342
+ ): SkeletonHints {
343
+ // Determine layout type from route - apps can override via custom signals
344
+ let layout: SkeletonHints['layout'] = 'custom';
345
+
346
+ // Simple path-based heuristics (can be overridden by app)
347
+ if (route === '/' || route.includes('dashboard')) {
348
+ layout = 'dashboard';
349
+ } else if (route.includes('chat') || route.includes('message')) {
350
+ layout = 'chat';
351
+ } else if (route.includes('setting') || route.includes('config')) {
352
+ layout = 'settings';
353
+ } else if (route.includes('tool')) {
354
+ layout = 'tools';
355
+ }
356
+
357
+ // Estimate height based on viewport and content
358
+ const baseHeight = context.viewport.height;
359
+ const contentMultiplier = tree.nodes.size > 10 ? 1.5 : 1;
360
+ const estimatedHeight = Math.round(baseHeight * contentMultiplier);
361
+
362
+ // Compute section hints
363
+ const sections = tree.getChildren(tree.rootId).map((child, i) => ({
364
+ id: child.id,
365
+ height: Math.round(estimatedHeight / (tree.nodes.size || 1)),
366
+ priority: i + 1,
367
+ }));
368
+
369
+ return {
370
+ layout,
371
+ estimatedHeight,
372
+ sections,
373
+ };
374
+ }
375
+
376
+ // ============================================================================
377
+ // Prefetch Depth by Connection
378
+ // ============================================================================
379
+
380
+ function getPrefetchDepth(context: UserContext): {
381
+ prefetch: number;
382
+ prerender: number;
383
+ } {
384
+ switch (context.connection) {
385
+ case 'fast':
386
+ case '4g':
387
+ return { prefetch: 5, prerender: 1 };
388
+ case '3g':
389
+ return { prefetch: 3, prerender: 0 };
390
+ case '2g':
391
+ return { prefetch: 1, prerender: 0 };
392
+ case 'slow-2g':
393
+ return { prefetch: 0, prerender: 0 };
394
+ default:
395
+ return { prefetch: 3, prerender: 0 };
396
+ }
397
+ }
398
+
399
+ // ============================================================================
400
+ // Heuristic Adapter Implementation
401
+ // ============================================================================
402
+
403
+ export class HeuristicAdapter implements RouterAdapter {
404
+ name = 'heuristic';
405
+ private config: Required<HeuristicAdapterConfig>;
406
+
407
+ constructor(config: HeuristicAdapterConfig = {}) {
408
+ this.config = {
409
+ ...DEFAULT_CONFIG,
410
+ ...config,
411
+ tierFeatures: config.tierFeatures ?? DEFAULT_CONFIG.tierFeatures,
412
+ signals: config.signals ?? DEFAULT_CONFIG.signals,
413
+ };
414
+ }
415
+
416
+ async route(
417
+ path: string,
418
+ context: UserContext,
419
+ tree: ComponentTree,
420
+ ): Promise<RouteDecision> {
421
+ const startTime = Date.now();
422
+
423
+ // Generate session ID
424
+ const sessionId = this.generateSessionId(path, context);
425
+
426
+ // Compute feature flags from tier
427
+ const featureFlags = { ...this.config.tierFeatures[context.tier] };
428
+
429
+ // Compute theme - use custom processor or default
430
+ const theme = this.config.signals.deriveTheme
431
+ ? this.config.signals.deriveTheme(context)
432
+ : defaultDeriveTheme(context);
433
+
434
+ // Compute accent - use custom processor or default
435
+ const accent = this.config.signals.deriveAccent
436
+ ? this.config.signals.deriveAccent(context)
437
+ : this.config.defaultAccent;
438
+
439
+ // Compute density
440
+ const density = determineDensity(context);
441
+
442
+ // Relevance scoring - use custom or default
443
+ const scoreRelevance =
444
+ this.config.signals.scoreRelevance ?? defaultScoreRelevance;
445
+
446
+ // Order components by relevance
447
+ const componentOrder = orderComponentsByRelevance(
448
+ tree,
449
+ context,
450
+ scoreRelevance,
451
+ );
452
+
453
+ // Find hidden components
454
+ const hiddenComponents = findHiddenComponents(
455
+ tree,
456
+ context,
457
+ scoreRelevance,
458
+ );
459
+
460
+ // Predict likely next paths - use custom or default
461
+ const predictions = this.config.signals.predictNavigation
462
+ ? this.config.signals.predictNavigation(path, context)
463
+ : defaultPredictNavigation(
464
+ path,
465
+ context,
466
+ this.config.defaultPaths,
467
+ this.config.maxSpeculationPaths,
468
+ );
469
+
470
+ const { prefetch: prefetchDepth, prerender: prerenderCount } =
471
+ getPrefetchDepth(context);
472
+
473
+ const prefetch = predictions.slice(0, prefetchDepth);
474
+ const prerender = predictions.slice(0, prerenderCount);
475
+
476
+ // Compute skeleton hints
477
+ const skeleton = computeSkeletonHints(path, context, tree);
478
+
479
+ return {
480
+ route: path,
481
+ sessionId,
482
+ componentOrder,
483
+ hiddenComponents,
484
+ featureFlags,
485
+ theme,
486
+ accent,
487
+ density,
488
+ prefetch,
489
+ prerender,
490
+ skeleton,
491
+ routedAt: startTime,
492
+ routerName: this.name,
493
+ confidence: 0.85, // Heuristic confidence
494
+ };
495
+ }
496
+
497
+ async speculate(
498
+ currentPath: string,
499
+ context: UserContext,
500
+ ): Promise<string[]> {
501
+ return this.config.signals.predictNavigation
502
+ ? this.config.signals.predictNavigation(currentPath, context)
503
+ : defaultPredictNavigation(
504
+ currentPath,
505
+ context,
506
+ this.config.defaultPaths,
507
+ this.config.maxSpeculationPaths,
508
+ );
509
+ }
510
+
511
+ personalizeTree(tree: ComponentTree, decision: RouteDecision): ComponentTree {
512
+ const cloned = tree.clone();
513
+
514
+ // Hide components that should be hidden
515
+ if (decision.hiddenComponents) {
516
+ for (const id of decision.hiddenComponents) {
517
+ const node = cloned.getNode(id);
518
+ if (node) {
519
+ node.defaultHidden = true;
520
+ }
521
+ }
522
+ }
523
+
524
+ return cloned;
525
+ }
526
+
527
+ emotionToAccent(emotionState: EmotionState): string {
528
+ // If app provided a custom deriveAccent, use it
529
+ if (this.config.signals.deriveAccent) {
530
+ return this.config.signals.deriveAccent({
531
+ emotionState,
532
+ // Provide minimal context for just the emotion
533
+ tier: 'free',
534
+ recentPages: [],
535
+ dwellTimes: new Map(),
536
+ clickPatterns: [],
537
+ preferences: {},
538
+ viewport: { width: 0, height: 0 },
539
+ connection: 'fast',
540
+ reducedMotion: false,
541
+ localHour: 12,
542
+ timezone: 'UTC',
543
+ isNewSession: true,
544
+ });
545
+ }
546
+
547
+ return this.config.defaultAccent;
548
+ }
549
+
550
+ private generateSessionId(path: string, context: UserContext): string {
551
+ const base = path.replace(/^\/|\/$/g, '').replace(/\//g, '-') || 'index';
552
+ const userId = context.userId || 'anon';
553
+ const sessionPrefix = context.sessionId || Date.now().toString(36);
554
+
555
+ return `${base}-${userId.slice(0, 8)}-${sessionPrefix.slice(0, 8)}`;
556
+ }
557
+ }