@directive-run/knowledge 0.2.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 (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -0
  3. package/ai/ai-adapters.md +250 -0
  4. package/ai/ai-agents-streaming.md +269 -0
  5. package/ai/ai-budget-resilience.md +235 -0
  6. package/ai/ai-communication.md +281 -0
  7. package/ai/ai-debug-observability.md +243 -0
  8. package/ai/ai-guardrails-memory.md +332 -0
  9. package/ai/ai-mcp-rag.md +288 -0
  10. package/ai/ai-multi-agent.md +274 -0
  11. package/ai/ai-orchestrator.md +227 -0
  12. package/ai/ai-security.md +293 -0
  13. package/ai/ai-tasks.md +261 -0
  14. package/ai/ai-testing-evals.md +378 -0
  15. package/api-skeleton.md +5 -0
  16. package/core/anti-patterns.md +382 -0
  17. package/core/constraints.md +263 -0
  18. package/core/core-patterns.md +228 -0
  19. package/core/error-boundaries.md +322 -0
  20. package/core/multi-module.md +315 -0
  21. package/core/naming.md +283 -0
  22. package/core/plugins.md +344 -0
  23. package/core/react-adapter.md +262 -0
  24. package/core/resolvers.md +357 -0
  25. package/core/schema-types.md +262 -0
  26. package/core/system-api.md +271 -0
  27. package/core/testing.md +257 -0
  28. package/core/time-travel.md +238 -0
  29. package/dist/index.cjs +111 -0
  30. package/dist/index.cjs.map +1 -0
  31. package/dist/index.d.cts +10 -0
  32. package/dist/index.d.ts +10 -0
  33. package/dist/index.js +102 -0
  34. package/dist/index.js.map +1 -0
  35. package/examples/ab-testing.ts +385 -0
  36. package/examples/ai-checkpoint.ts +509 -0
  37. package/examples/ai-guardrails.ts +319 -0
  38. package/examples/ai-orchestrator.ts +589 -0
  39. package/examples/async-chains.ts +287 -0
  40. package/examples/auth-flow.ts +371 -0
  41. package/examples/batch-resolver.ts +341 -0
  42. package/examples/checkers.ts +589 -0
  43. package/examples/contact-form.ts +176 -0
  44. package/examples/counter.ts +393 -0
  45. package/examples/dashboard-loader.ts +512 -0
  46. package/examples/debounce-constraints.ts +105 -0
  47. package/examples/dynamic-modules.ts +293 -0
  48. package/examples/error-boundaries.ts +430 -0
  49. package/examples/feature-flags.ts +220 -0
  50. package/examples/form-wizard.ts +347 -0
  51. package/examples/fraud-analysis.ts +663 -0
  52. package/examples/goal-heist.ts +341 -0
  53. package/examples/multi-module.ts +57 -0
  54. package/examples/newsletter.ts +241 -0
  55. package/examples/notifications.ts +210 -0
  56. package/examples/optimistic-updates.ts +317 -0
  57. package/examples/pagination.ts +260 -0
  58. package/examples/permissions.ts +337 -0
  59. package/examples/provider-routing.ts +403 -0
  60. package/examples/server.ts +316 -0
  61. package/examples/shopping-cart.ts +422 -0
  62. package/examples/sudoku.ts +630 -0
  63. package/examples/theme-locale.ts +204 -0
  64. package/examples/time-machine.ts +225 -0
  65. package/examples/topic-guard.ts +306 -0
  66. package/examples/url-sync.ts +333 -0
  67. package/examples/websocket.ts +404 -0
  68. package/package.json +65 -0
@@ -0,0 +1,293 @@
1
+ // Example: dynamic-modules
2
+ // Source: examples/dynamic-modules/src/modules.ts
3
+ // Pure module file — no DOM wiring
4
+
5
+ /**
6
+ * Dynamic Modules — Directive Module Definitions
7
+ *
8
+ * Dashboard module (always loaded) + 3 dynamic modules (Counter, Weather, Dice).
9
+ * Demonstrates runtime module registration, namespaced fact access,
10
+ * constraints, resolvers, and derivations across independent modules.
11
+ */
12
+
13
+ import { type ModuleSchema, createModule, t } from "@directive-run/core";
14
+ import { mockFetchWeather } from "./mock-weather.js";
15
+
16
+ // ============================================================================
17
+ // Types
18
+ // ============================================================================
19
+
20
+ export interface EventLogEntry {
21
+ timestamp: number;
22
+ event: string;
23
+ detail: string;
24
+ }
25
+
26
+ // ============================================================================
27
+ // Helpers
28
+ // ============================================================================
29
+
30
+ function addLogEntry(facts: any, event: string, detail: string): void {
31
+ const log = [...(facts.eventLog as EventLogEntry[])];
32
+ log.push({ timestamp: Date.now(), event, detail });
33
+ if (log.length > 50) {
34
+ log.splice(0, log.length - 50);
35
+ }
36
+ facts.eventLog = log;
37
+ }
38
+
39
+ // ============================================================================
40
+ // Dashboard Module (core, always loaded)
41
+ // ============================================================================
42
+
43
+ export const dashboardSchema = {
44
+ facts: {
45
+ loadedModules: t.object<string[]>(),
46
+ eventLog: t.object<EventLogEntry[]>(),
47
+ },
48
+ derivations: {
49
+ loadedCount: t.number(),
50
+ },
51
+ events: {
52
+ moduleLoaded: { name: t.string() },
53
+ },
54
+ requirements: {},
55
+ } satisfies ModuleSchema;
56
+
57
+ export const dashboardModule = createModule("dashboard", {
58
+ schema: dashboardSchema,
59
+
60
+ init: (facts) => {
61
+ facts.loadedModules = [];
62
+ facts.eventLog = [];
63
+ },
64
+
65
+ derive: {
66
+ loadedCount: (facts) => (facts.loadedModules as string[]).length,
67
+ },
68
+
69
+ events: {
70
+ moduleLoaded: (facts, { name }) => {
71
+ facts.loadedModules = [...(facts.loadedModules as string[]), name];
72
+ addLogEntry(facts, "loaded", `Loaded "${name}" module`);
73
+ },
74
+ },
75
+ });
76
+
77
+ // ============================================================================
78
+ // Counter Module (dynamic)
79
+ // ============================================================================
80
+
81
+ export const counterSchema = {
82
+ facts: {
83
+ count: t.number(),
84
+ step: t.number(),
85
+ },
86
+ derivations: {
87
+ isNearMax: t.boolean(),
88
+ },
89
+ events: {
90
+ increment: {},
91
+ decrement: {},
92
+ setStep: { value: t.number() },
93
+ },
94
+ requirements: {
95
+ COUNTER_RESET: {},
96
+ },
97
+ } satisfies ModuleSchema;
98
+
99
+ export const counterModule = createModule("counter", {
100
+ schema: counterSchema,
101
+
102
+ init: (facts) => {
103
+ facts.count = 0;
104
+ facts.step = 1;
105
+ },
106
+
107
+ derive: {
108
+ isNearMax: (facts) => (facts.count as number) >= 90,
109
+ },
110
+
111
+ events: {
112
+ increment: (facts) => {
113
+ facts.count = (facts.count as number) + (facts.step as number);
114
+ },
115
+ decrement: (facts) => {
116
+ facts.count = Math.max(
117
+ 0,
118
+ (facts.count as number) - (facts.step as number),
119
+ );
120
+ },
121
+ setStep: (facts, { value }) => {
122
+ facts.step = value;
123
+ },
124
+ },
125
+
126
+ constraints: {
127
+ overflow: {
128
+ priority: 100,
129
+ when: (facts) => (facts.count as number) >= 100,
130
+ require: () => ({ type: "COUNTER_RESET" }),
131
+ },
132
+ },
133
+
134
+ resolvers: {
135
+ counterReset: {
136
+ requirement: "COUNTER_RESET",
137
+ resolve: async (_req, context) => {
138
+ context.facts.count = 0;
139
+ },
140
+ },
141
+ },
142
+ });
143
+
144
+ // ============================================================================
145
+ // Weather Module (dynamic)
146
+ // ============================================================================
147
+
148
+ export const weatherSchema = {
149
+ facts: {
150
+ city: t.string(),
151
+ temperature: t.number(),
152
+ condition: t.string(),
153
+ humidity: t.number(),
154
+ isLoading: t.boolean(),
155
+ lastFetchedCity: t.string(),
156
+ },
157
+ derivations: {
158
+ summary: t.string(),
159
+ hasFetched: t.boolean(),
160
+ },
161
+ events: {
162
+ setCity: { value: t.string() },
163
+ refresh: {},
164
+ },
165
+ requirements: {
166
+ FETCH_WEATHER: {
167
+ city: t.string(),
168
+ },
169
+ },
170
+ } satisfies ModuleSchema;
171
+
172
+ export const weatherModule = createModule("weather", {
173
+ schema: weatherSchema,
174
+
175
+ init: (facts) => {
176
+ facts.city = "";
177
+ facts.temperature = 0;
178
+ facts.condition = "";
179
+ facts.humidity = 0;
180
+ facts.isLoading = false;
181
+ facts.lastFetchedCity = "";
182
+ },
183
+
184
+ derive: {
185
+ summary: (facts) => {
186
+ if ((facts.city as string) === "") {
187
+ return "";
188
+ }
189
+
190
+ return `${facts.temperature}\u00B0F, ${facts.condition}`;
191
+ },
192
+ hasFetched: (facts) => (facts.lastFetchedCity as string) !== "",
193
+ },
194
+
195
+ events: {
196
+ setCity: (facts, { value }) => {
197
+ facts.city = value;
198
+ },
199
+ refresh: (facts) => {
200
+ facts.lastFetchedCity = "";
201
+ },
202
+ },
203
+
204
+ constraints: {
205
+ needsFetch: {
206
+ priority: 100,
207
+ when: (facts) =>
208
+ (facts.city as string).length >= 2 &&
209
+ facts.city !== facts.lastFetchedCity &&
210
+ !(facts.isLoading as boolean),
211
+ require: (facts) => ({
212
+ type: "FETCH_WEATHER",
213
+ city: facts.city as string,
214
+ }),
215
+ },
216
+ },
217
+
218
+ resolvers: {
219
+ fetchWeather: {
220
+ requirement: "FETCH_WEATHER",
221
+ key: (req) => `weather-${req.city}`,
222
+ timeout: 10000,
223
+ resolve: async (req, context) => {
224
+ context.facts.isLoading = true;
225
+
226
+ const data = await mockFetchWeather(req.city, 800);
227
+
228
+ // Stale check: only apply if city still matches
229
+ if ((context.facts.city as string) === req.city) {
230
+ context.facts.temperature = data.temperature;
231
+ context.facts.condition = data.condition;
232
+ context.facts.humidity = data.humidity;
233
+ context.facts.lastFetchedCity = req.city;
234
+ }
235
+
236
+ context.facts.isLoading = false;
237
+ },
238
+ },
239
+ },
240
+ });
241
+
242
+ // ============================================================================
243
+ // Dice Module (dynamic)
244
+ // ============================================================================
245
+
246
+ export const diceSchema = {
247
+ facts: {
248
+ die1: t.number(),
249
+ die2: t.number(),
250
+ rollCount: t.number(),
251
+ },
252
+ derivations: {
253
+ total: t.number(),
254
+ isDoubles: t.boolean(),
255
+ },
256
+ events: {
257
+ roll: {},
258
+ },
259
+ requirements: {},
260
+ } satisfies ModuleSchema;
261
+
262
+ export const diceModule = createModule("dice", {
263
+ schema: diceSchema,
264
+
265
+ init: (facts) => {
266
+ facts.die1 = 1;
267
+ facts.die2 = 1;
268
+ facts.rollCount = 0;
269
+ },
270
+
271
+ derive: {
272
+ total: (facts) => (facts.die1 as number) + (facts.die2 as number),
273
+ isDoubles: (facts) => facts.die1 === facts.die2,
274
+ },
275
+
276
+ events: {
277
+ roll: (facts) => {
278
+ facts.die1 = Math.floor(Math.random() * 6) + 1;
279
+ facts.die2 = Math.floor(Math.random() * 6) + 1;
280
+ facts.rollCount = (facts.rollCount as number) + 1;
281
+ },
282
+ },
283
+ });
284
+
285
+ // ============================================================================
286
+ // Module Registry
287
+ // ============================================================================
288
+
289
+ export const moduleRegistry: Record<string, { module: any; label: string }> = {
290
+ counter: { module: counterModule, label: "Counter" },
291
+ weather: { module: weatherModule, label: "Weather" },
292
+ dice: { module: diceModule, label: "Dice" },
293
+ };
@@ -0,0 +1,430 @@
1
+ // Example: error-boundaries
2
+ // Source: examples/error-boundaries/src/main.ts
3
+ // Extracted for AI rules — DOM wiring stripped
4
+
5
+ /**
6
+ * Resilient API Dashboard — Error Boundaries, Retry, Circuit Breaker, Performance
7
+ *
8
+ * 3 simulated API services with configurable failure rates. Users inject errors
9
+ * and watch recovery strategies, circuit breaker state transitions, retry-later
10
+ * backoff, and performance metrics.
11
+ */
12
+
13
+ import {
14
+ type ModuleSchema,
15
+ type RecoveryStrategy,
16
+ createModule,
17
+ createSystem,
18
+ t,
19
+ } from "@directive-run/core";
20
+ import { devtoolsPlugin, performancePlugin } from "@directive-run/core/plugins";
21
+ import {
22
+ type CircuitState,
23
+ createCircuitBreaker,
24
+ } from "@directive-run/core/plugins";
25
+
26
+ // ============================================================================
27
+ // Types
28
+ // ============================================================================
29
+
30
+ interface ServiceState {
31
+ name: string;
32
+ status: "idle" | "loading" | "success" | "error";
33
+ lastResult: string;
34
+ errorCount: number;
35
+ successCount: number;
36
+ lastError: string;
37
+ }
38
+
39
+ interface TimelineEntry {
40
+ time: number;
41
+ event: string;
42
+ detail: string;
43
+ type: "info" | "error" | "retry" | "circuit" | "recovery" | "success";
44
+ }
45
+
46
+ // ============================================================================
47
+ // Circuit Breakers (one per service)
48
+ // ============================================================================
49
+
50
+ const timeline: TimelineEntry[] = [];
51
+
52
+ function addTimeline(
53
+ event: string,
54
+ detail: string,
55
+ type: TimelineEntry["type"],
56
+ ) {
57
+ timeline.unshift({ time: Date.now(), event, detail, type });
58
+ if (timeline.length > 50) {
59
+ timeline.length = 50;
60
+ }
61
+ }
62
+
63
+ const circuitBreakers = {
64
+ users: createCircuitBreaker({
65
+ name: "users-api",
66
+ failureThreshold: 3,
67
+ recoveryTimeMs: 5000,
68
+ halfOpenMaxRequests: 2,
69
+ onStateChange: (from, to) => {
70
+ },
71
+ }),
72
+ orders: createCircuitBreaker({
73
+ name: "orders-api",
74
+ failureThreshold: 3,
75
+ recoveryTimeMs: 5000,
76
+ halfOpenMaxRequests: 2,
77
+ onStateChange: (from, to) => {
78
+ },
79
+ }),
80
+ analytics: createCircuitBreaker({
81
+ name: "analytics-api",
82
+ failureThreshold: 3,
83
+ recoveryTimeMs: 5000,
84
+ halfOpenMaxRequests: 2,
85
+ onStateChange: (from, to) => {
86
+ },
87
+ }),
88
+ };
89
+
90
+ // ============================================================================
91
+ // Schema
92
+ // ============================================================================
93
+
94
+ const schema = {
95
+ facts: {
96
+ usersService: t.object<ServiceState>(),
97
+ ordersService: t.object<ServiceState>(),
98
+ analyticsService: t.object<ServiceState>(),
99
+ strategy: t.string<RecoveryStrategy>(),
100
+ usersFailRate: t.number(),
101
+ ordersFailRate: t.number(),
102
+ analyticsFailRate: t.number(),
103
+ retryQueueCount: t.number(),
104
+ totalErrors: t.number(),
105
+ totalRecoveries: t.number(),
106
+ },
107
+ derivations: {
108
+ usersCircuitState: t.string<CircuitState>(),
109
+ ordersCircuitState: t.string<CircuitState>(),
110
+ analyticsCircuitState: t.string<CircuitState>(),
111
+ errorRate: t.number(),
112
+ allServicesHealthy: t.boolean(),
113
+ },
114
+ events: {
115
+ fetchUsers: {},
116
+ fetchOrders: {},
117
+ fetchAnalytics: {},
118
+ fetchAll: {},
119
+ setStrategy: { value: t.string<RecoveryStrategy>() },
120
+ setUsersFailRate: { value: t.number() },
121
+ setOrdersFailRate: { value: t.number() },
122
+ setAnalyticsFailRate: { value: t.number() },
123
+ resetAll: {},
124
+ },
125
+ requirements: {
126
+ FETCH_SERVICE: { service: t.string(), failRate: t.number() },
127
+ },
128
+ } satisfies ModuleSchema;
129
+
130
+ // ============================================================================
131
+ // Module
132
+ // ============================================================================
133
+
134
+ const dashboardModule = createModule("dashboard", {
135
+ schema,
136
+
137
+ init: (facts) => {
138
+ const defaultService: ServiceState = {
139
+ name: "",
140
+ status: "idle",
141
+ lastResult: "",
142
+ errorCount: 0,
143
+ successCount: 0,
144
+ lastError: "",
145
+ };
146
+ facts.usersService = { ...defaultService, name: "Users API" };
147
+ facts.ordersService = { ...defaultService, name: "Orders API" };
148
+ facts.analyticsService = { ...defaultService, name: "Analytics API" };
149
+ facts.strategy = "retry-later";
150
+ facts.usersFailRate = 0;
151
+ facts.ordersFailRate = 0;
152
+ facts.analyticsFailRate = 0;
153
+ facts.retryQueueCount = 0;
154
+ facts.totalErrors = 0;
155
+ facts.totalRecoveries = 0;
156
+ },
157
+
158
+ derive: {
159
+ usersCircuitState: () => circuitBreakers.users.getState(),
160
+ ordersCircuitState: () => circuitBreakers.orders.getState(),
161
+ analyticsCircuitState: () => circuitBreakers.analytics.getState(),
162
+ errorRate: (facts) => {
163
+ const total =
164
+ facts.usersService.errorCount +
165
+ facts.usersService.successCount +
166
+ facts.ordersService.errorCount +
167
+ facts.ordersService.successCount +
168
+ facts.analyticsService.errorCount +
169
+ facts.analyticsService.successCount;
170
+
171
+ if (total === 0) {
172
+ return 0;
173
+ }
174
+
175
+ const errors =
176
+ facts.usersService.errorCount +
177
+ facts.ordersService.errorCount +
178
+ facts.analyticsService.errorCount;
179
+
180
+ return Math.round((errors / total) * 100);
181
+ },
182
+ allServicesHealthy: (facts) =>
183
+ facts.usersService.status !== "error" &&
184
+ facts.ordersService.status !== "error" &&
185
+ facts.analyticsService.status !== "error",
186
+ },
187
+
188
+ events: {
189
+ fetchUsers: (facts) => {
190
+ facts.usersService = { ...facts.usersService, status: "loading" };
191
+ },
192
+ fetchOrders: (facts) => {
193
+ facts.ordersService = { ...facts.ordersService, status: "loading" };
194
+ },
195
+ fetchAnalytics: (facts) => {
196
+ facts.analyticsService = { ...facts.analyticsService, status: "loading" };
197
+ },
198
+ fetchAll: (facts) => {
199
+ facts.usersService = { ...facts.usersService, status: "loading" };
200
+ facts.ordersService = { ...facts.ordersService, status: "loading" };
201
+ facts.analyticsService = { ...facts.analyticsService, status: "loading" };
202
+ },
203
+ setStrategy: (facts, { value }) => {
204
+ facts.strategy = value;
205
+ },
206
+ setUsersFailRate: (facts, { value }) => {
207
+ facts.usersFailRate = value;
208
+ },
209
+ setOrdersFailRate: (facts, { value }) => {
210
+ facts.ordersFailRate = value;
211
+ },
212
+ setAnalyticsFailRate: (facts, { value }) => {
213
+ facts.analyticsFailRate = value;
214
+ },
215
+ resetAll: (facts) => {
216
+ const defaultService: ServiceState = {
217
+ name: "",
218
+ status: "idle",
219
+ lastResult: "",
220
+ errorCount: 0,
221
+ successCount: 0,
222
+ lastError: "",
223
+ };
224
+ facts.usersService = { ...defaultService, name: "Users API" };
225
+ facts.ordersService = { ...defaultService, name: "Orders API" };
226
+ facts.analyticsService = { ...defaultService, name: "Analytics API" };
227
+ facts.retryQueueCount = 0;
228
+ facts.totalErrors = 0;
229
+ facts.totalRecoveries = 0;
230
+ circuitBreakers.users.reset();
231
+ circuitBreakers.orders.reset();
232
+ circuitBreakers.analytics.reset();
233
+ timeline.length = 0;
234
+ },
235
+ },
236
+
237
+ constraints: {
238
+ usersNeedsLoad: {
239
+ priority: 50,
240
+ when: (facts) => facts.usersService.status === "loading",
241
+ require: (facts) => ({
242
+ type: "FETCH_SERVICE",
243
+ service: "users",
244
+ failRate: facts.usersFailRate,
245
+ }),
246
+ },
247
+ ordersNeedsLoad: {
248
+ priority: 50,
249
+ when: (facts) => facts.ordersService.status === "loading",
250
+ require: (facts) => ({
251
+ type: "FETCH_SERVICE",
252
+ service: "orders",
253
+ failRate: facts.ordersFailRate,
254
+ }),
255
+ },
256
+ analyticsNeedsLoad: {
257
+ priority: 50,
258
+ when: (facts) => facts.analyticsService.status === "loading",
259
+ require: (facts) => ({
260
+ type: "FETCH_SERVICE",
261
+ service: "analytics",
262
+ failRate: facts.analyticsFailRate,
263
+ }),
264
+ },
265
+ },
266
+
267
+ resolvers: {
268
+ fetchService: {
269
+ requirement: "FETCH_SERVICE",
270
+ retry: { attempts: 2, backoff: "exponential", initialDelay: 200 },
271
+ resolve: async (req, context) => {
272
+ const { service, failRate } = req;
273
+ const breaker =
274
+ circuitBreakers[service as keyof typeof circuitBreakers];
275
+ const serviceKey = `${service}Service` as
276
+ | "usersService"
277
+ | "ordersService"
278
+ | "analyticsService";
279
+
280
+ try {
281
+ await breaker.execute(async () => {
282
+ // Simulate API call
283
+ await new Promise((resolve) =>
284
+ setTimeout(resolve, 200 + Math.random() * 300),
285
+ );
286
+
287
+ if (Math.random() * 100 < failRate) {
288
+ throw new Error(`${service} API: simulated failure`);
289
+ }
290
+ });
291
+
292
+ // Success
293
+ const current = context.facts[serviceKey] as ServiceState;
294
+ context.facts[serviceKey] = {
295
+ ...current,
296
+ status: "success",
297
+ lastResult: `Loaded at ${new Date().toLocaleTimeString()}`,
298
+ successCount: current.successCount + 1,
299
+ };
300
+ } catch (error) {
301
+ const current = context.facts[serviceKey] as ServiceState;
302
+ const msg = error instanceof Error ? error.message : String(error);
303
+ context.facts[serviceKey] = {
304
+ ...current,
305
+ status: "error",
306
+ lastError: msg,
307
+ errorCount: current.errorCount + 1,
308
+ };
309
+ context.facts.totalErrors = context.facts.totalErrors + 1;
310
+
311
+ // Re-throw so the error boundary handles recovery
312
+ throw error;
313
+ }
314
+ },
315
+ },
316
+ },
317
+ });
318
+
319
+ // ============================================================================
320
+ // Performance Plugin
321
+ // ============================================================================
322
+
323
+ const perf = performancePlugin({
324
+ onSlowResolver: (id, ms) => {
325
+ },
326
+ });
327
+
328
+ // ============================================================================
329
+ // System
330
+ // ============================================================================
331
+
332
+ let currentStrategy: RecoveryStrategy = "retry-later";
333
+
334
+ const system = createSystem({
335
+ module: dashboardModule,
336
+ debug: { runHistory: true },
337
+ plugins: [perf, devtoolsPlugin({ name: "error-boundaries" })],
338
+ errorBoundary: {
339
+ onResolverError: (_error, resolver) => {
340
+ "recovery",
341
+ `${resolver}: strategy=${currentStrategy}`,
342
+ "recovery",
343
+ );
344
+
345
+ return currentStrategy;
346
+ },
347
+ onConstraintError: "skip",
348
+ onEffectError: "skip",
349
+ retryLater: {
350
+ delayMs: 1000,
351
+ maxRetries: 3,
352
+ backoffMultiplier: 2,
353
+ },
354
+ onError: (error) => {
355
+ },
356
+ },
357
+ });
358
+ system.start();
359
+
360
+ // Track strategy changes to update error boundary (via re-dispatch)
361
+ system.subscribe(["strategy"], () => {
362
+ const newStrategy = system.facts.strategy as RecoveryStrategy;
363
+ if (newStrategy !== currentStrategy) {
364
+ currentStrategy = newStrategy;
365
+ }
366
+ });
367
+
368
+ // ============================================================================
369
+ // DOM References
370
+ // ============================================================================
371
+
372
+ // Service cards
373
+
374
+ // Sliders
375
+ "eb-users-failrate",
376
+ "eb-orders-failrate",
377
+ "eb-analytics-failrate",
378
+
379
+ // Strategy dropdown
380
+ "eb-strategy",
381
+
382
+ // Timeline
383
+
384
+ // ============================================================================
385
+ // Render
386
+ // ============================================================================
387
+
388
+ function escapeHtml(text: string): string {
389
+
390
+ return div.innerHTML;
391
+ }
392
+
393
+ service: ServiceState,
394
+ ): void {
395
+ if (service.lastError) {
396
+ } else {
397
+ }
398
+ }
399
+
400
+
401
+ // ============================================================================
402
+ // Subscribe
403
+ // ============================================================================
404
+
405
+ const allKeys = [
406
+ ...Object.keys(schema.facts),
407
+ ...Object.keys(schema.derivations),
408
+ ];
409
+ system.subscribe(allKeys, render);
410
+
411
+ // Periodic refresh for circuit breaker state transitions + retry queue
412
+ setInterval(() => {
413
+ render();
414
+ }, 1000);
415
+
416
+ // ============================================================================
417
+ // Controls
418
+ // ============================================================================
419
+
420
+ // Fetch buttons
421
+
422
+ // Strategy selector
423
+
424
+ // Sliders
425
+
426
+ // ============================================================================
427
+ // Initial Render
428
+ // ============================================================================
429
+
430
+ render();