@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.
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/ai/ai-adapters.md +250 -0
- package/ai/ai-agents-streaming.md +269 -0
- package/ai/ai-budget-resilience.md +235 -0
- package/ai/ai-communication.md +281 -0
- package/ai/ai-debug-observability.md +243 -0
- package/ai/ai-guardrails-memory.md +332 -0
- package/ai/ai-mcp-rag.md +288 -0
- package/ai/ai-multi-agent.md +274 -0
- package/ai/ai-orchestrator.md +227 -0
- package/ai/ai-security.md +293 -0
- package/ai/ai-tasks.md +261 -0
- package/ai/ai-testing-evals.md +378 -0
- package/api-skeleton.md +5 -0
- package/core/anti-patterns.md +382 -0
- package/core/constraints.md +263 -0
- package/core/core-patterns.md +228 -0
- package/core/error-boundaries.md +322 -0
- package/core/multi-module.md +315 -0
- package/core/naming.md +283 -0
- package/core/plugins.md +344 -0
- package/core/react-adapter.md +262 -0
- package/core/resolvers.md +357 -0
- package/core/schema-types.md +262 -0
- package/core/system-api.md +271 -0
- package/core/testing.md +257 -0
- package/core/time-travel.md +238 -0
- package/dist/index.cjs +111 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +102 -0
- package/dist/index.js.map +1 -0
- package/examples/ab-testing.ts +385 -0
- package/examples/ai-checkpoint.ts +509 -0
- package/examples/ai-guardrails.ts +319 -0
- package/examples/ai-orchestrator.ts +589 -0
- package/examples/async-chains.ts +287 -0
- package/examples/auth-flow.ts +371 -0
- package/examples/batch-resolver.ts +341 -0
- package/examples/checkers.ts +589 -0
- package/examples/contact-form.ts +176 -0
- package/examples/counter.ts +393 -0
- package/examples/dashboard-loader.ts +512 -0
- package/examples/debounce-constraints.ts +105 -0
- package/examples/dynamic-modules.ts +293 -0
- package/examples/error-boundaries.ts +430 -0
- package/examples/feature-flags.ts +220 -0
- package/examples/form-wizard.ts +347 -0
- package/examples/fraud-analysis.ts +663 -0
- package/examples/goal-heist.ts +341 -0
- package/examples/multi-module.ts +57 -0
- package/examples/newsletter.ts +241 -0
- package/examples/notifications.ts +210 -0
- package/examples/optimistic-updates.ts +317 -0
- package/examples/pagination.ts +260 -0
- package/examples/permissions.ts +337 -0
- package/examples/provider-routing.ts +403 -0
- package/examples/server.ts +316 -0
- package/examples/shopping-cart.ts +422 -0
- package/examples/sudoku.ts +630 -0
- package/examples/theme-locale.ts +204 -0
- package/examples/time-machine.ts +225 -0
- package/examples/topic-guard.ts +306 -0
- package/examples/url-sync.ts +333 -0
- package/examples/websocket.ts +404 -0
- package/package.json +65 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
// Example: ab-testing
|
|
2
|
+
// Source: examples/ab-testing/src/main.ts
|
|
3
|
+
// Extracted for AI rules — DOM wiring stripped
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A/B Testing Engine — DOM Rendering & System Wiring
|
|
7
|
+
*
|
|
8
|
+
* Creates the Directive system, subscribes to state changes,
|
|
9
|
+
* renders experiment cards and event timeline.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
type ModuleSchema,
|
|
14
|
+
createModule,
|
|
15
|
+
createSystem,
|
|
16
|
+
t,
|
|
17
|
+
} from "@directive-run/core";
|
|
18
|
+
import { devtoolsPlugin } from "@directive-run/core/plugins";
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Types
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
interface Variant {
|
|
25
|
+
id: string;
|
|
26
|
+
weight: number;
|
|
27
|
+
label: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface Experiment {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
variants: Variant[];
|
|
34
|
+
active: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface TimelineEntry {
|
|
38
|
+
time: number;
|
|
39
|
+
event: string;
|
|
40
|
+
detail: string;
|
|
41
|
+
type: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// Deterministic Hash
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
function hashCode(str: string): number {
|
|
49
|
+
let hash = 0;
|
|
50
|
+
for (let i = 0; i < str.length; i++) {
|
|
51
|
+
const char = str.charCodeAt(i);
|
|
52
|
+
hash = ((hash << 5) - hash + char) | 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return Math.abs(hash);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function pickVariant(
|
|
59
|
+
userId: string,
|
|
60
|
+
experimentId: string,
|
|
61
|
+
variants: Variant[],
|
|
62
|
+
): string {
|
|
63
|
+
const hash = hashCode(`${userId}:${experimentId}`);
|
|
64
|
+
const totalWeight = variants.reduce((sum, v) => sum + v.weight, 0);
|
|
65
|
+
let roll = hash % totalWeight;
|
|
66
|
+
|
|
67
|
+
for (const variant of variants) {
|
|
68
|
+
roll -= variant.weight;
|
|
69
|
+
if (roll < 0) {
|
|
70
|
+
return variant.id;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return variants[variants.length - 1].id;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ============================================================================
|
|
78
|
+
// Timeline
|
|
79
|
+
// ============================================================================
|
|
80
|
+
|
|
81
|
+
const timeline: TimelineEntry[] = [];
|
|
82
|
+
|
|
83
|
+
function log(type: "event" | "constraint" | "resolver", msg: string) {
|
|
84
|
+
console.log(`[AB] [${type}] ${msg}`);
|
|
85
|
+
|
|
86
|
+
// Classify for timeline
|
|
87
|
+
let event = "";
|
|
88
|
+
let detail = msg;
|
|
89
|
+
let tlType = "register";
|
|
90
|
+
|
|
91
|
+
if (msg.startsWith("Registered")) {
|
|
92
|
+
event = "registered";
|
|
93
|
+
detail = msg.replace("Registered experiment: ", "");
|
|
94
|
+
tlType = "register";
|
|
95
|
+
} else if (msg.startsWith("Assigned") || msg.includes("→")) {
|
|
96
|
+
event = "assigned";
|
|
97
|
+
detail = msg;
|
|
98
|
+
tlType = "assign";
|
|
99
|
+
} else if (msg.includes("Exposure tracked")) {
|
|
100
|
+
event = "exposure";
|
|
101
|
+
detail = msg.replace("Exposure tracked: ", "");
|
|
102
|
+
tlType = "exposure";
|
|
103
|
+
} else if (msg.includes("Manual assignment")) {
|
|
104
|
+
event = "manual";
|
|
105
|
+
detail = msg.replace("Manual assignment: ", "");
|
|
106
|
+
tlType = "assign";
|
|
107
|
+
} else if (
|
|
108
|
+
msg.includes("Paused") ||
|
|
109
|
+
msg.includes("Resumed") ||
|
|
110
|
+
msg.includes("Reset")
|
|
111
|
+
) {
|
|
112
|
+
event = msg.toLowerCase().split(" ")[0];
|
|
113
|
+
detail = msg;
|
|
114
|
+
tlType = "control";
|
|
115
|
+
} else {
|
|
116
|
+
event = type;
|
|
117
|
+
detail = msg;
|
|
118
|
+
tlType = "register";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
timeline.unshift({ time: Date.now(), event, detail, type: tlType });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ============================================================================
|
|
125
|
+
// Schema
|
|
126
|
+
// ============================================================================
|
|
127
|
+
|
|
128
|
+
const schema = {
|
|
129
|
+
facts: {
|
|
130
|
+
experiments: t.object<Experiment[]>(),
|
|
131
|
+
assignments: t.object<Record<string, string>>(),
|
|
132
|
+
exposures: t.object<Record<string, number>>(),
|
|
133
|
+
userId: t.string(),
|
|
134
|
+
paused: t.boolean(),
|
|
135
|
+
},
|
|
136
|
+
derivations: {
|
|
137
|
+
activeExperiments: t.object<Experiment[]>(),
|
|
138
|
+
assignedCount: t.number(),
|
|
139
|
+
exposedCount: t.number(),
|
|
140
|
+
},
|
|
141
|
+
events: {
|
|
142
|
+
registerExperiment: {
|
|
143
|
+
id: t.string(),
|
|
144
|
+
name: t.string(),
|
|
145
|
+
variants: t.object<Variant[]>(),
|
|
146
|
+
},
|
|
147
|
+
assignVariant: { experimentId: t.string(), variantId: t.string() },
|
|
148
|
+
recordExposure: { experimentId: t.string() },
|
|
149
|
+
pauseAll: {},
|
|
150
|
+
resumeAll: {},
|
|
151
|
+
reset: {},
|
|
152
|
+
},
|
|
153
|
+
requirements: {
|
|
154
|
+
ASSIGN_VARIANT: { experimentId: t.string() },
|
|
155
|
+
TRACK_EXPOSURE: { experimentId: t.string(), variantId: t.string() },
|
|
156
|
+
},
|
|
157
|
+
} satisfies ModuleSchema;
|
|
158
|
+
|
|
159
|
+
// ============================================================================
|
|
160
|
+
// Module
|
|
161
|
+
// ============================================================================
|
|
162
|
+
|
|
163
|
+
const abTesting = createModule("ab-testing", {
|
|
164
|
+
schema,
|
|
165
|
+
|
|
166
|
+
init: (facts) => {
|
|
167
|
+
facts.experiments = [];
|
|
168
|
+
facts.assignments = {};
|
|
169
|
+
facts.exposures = {};
|
|
170
|
+
facts.userId = `user-${hashCode(String(Date.now())).toString(36)}`;
|
|
171
|
+
facts.paused = false;
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
derive: {
|
|
175
|
+
activeExperiments: (facts) =>
|
|
176
|
+
(facts.experiments as Experiment[]).filter(
|
|
177
|
+
(e) => e.active && !facts.paused,
|
|
178
|
+
),
|
|
179
|
+
assignedCount: (facts) => Object.keys(facts.assignments).length,
|
|
180
|
+
exposedCount: (facts) => Object.keys(facts.exposures).length,
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
events: {
|
|
184
|
+
registerExperiment: (facts, { id, name, variants }) => {
|
|
185
|
+
const experiments = facts.experiments as Experiment[];
|
|
186
|
+
if (!experiments.find((e: Experiment) => e.id === id)) {
|
|
187
|
+
facts.experiments = [
|
|
188
|
+
...experiments,
|
|
189
|
+
{ id, name, variants, active: true },
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
assignVariant: (facts, { experimentId, variantId }) => {
|
|
194
|
+
facts.assignments = { ...facts.assignments, [experimentId]: variantId };
|
|
195
|
+
},
|
|
196
|
+
recordExposure: (facts, { experimentId }) => {
|
|
197
|
+
facts.exposures = { ...facts.exposures, [experimentId]: Date.now() };
|
|
198
|
+
},
|
|
199
|
+
pauseAll: (facts) => {
|
|
200
|
+
facts.paused = true;
|
|
201
|
+
},
|
|
202
|
+
resumeAll: (facts) => {
|
|
203
|
+
facts.paused = false;
|
|
204
|
+
},
|
|
205
|
+
reset: (facts) => {
|
|
206
|
+
facts.assignments = {};
|
|
207
|
+
facts.exposures = {};
|
|
208
|
+
facts.paused = false;
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
constraints: {
|
|
213
|
+
needsAssignment: {
|
|
214
|
+
priority: 100,
|
|
215
|
+
when: (facts) => {
|
|
216
|
+
if (facts.paused) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
const experiments = facts.experiments as Experiment[];
|
|
220
|
+
const assignments = facts.assignments as Record<string, string>;
|
|
221
|
+
|
|
222
|
+
return experiments.some(
|
|
223
|
+
(e: Experiment) => e.active && !assignments[e.id],
|
|
224
|
+
);
|
|
225
|
+
},
|
|
226
|
+
require: (facts) => {
|
|
227
|
+
const experiments = facts.experiments as Experiment[];
|
|
228
|
+
const assignments = facts.assignments as Record<string, string>;
|
|
229
|
+
const unassigned = experiments.find(
|
|
230
|
+
(e: Experiment) => e.active && !assignments[e.id],
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
return { type: "ASSIGN_VARIANT", experimentId: unassigned!.id };
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
needsExposure: {
|
|
238
|
+
priority: 50,
|
|
239
|
+
when: (facts) => {
|
|
240
|
+
if (facts.paused) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
const assignments = facts.assignments as Record<string, string>;
|
|
244
|
+
const exposures = facts.exposures as Record<string, number>;
|
|
245
|
+
|
|
246
|
+
return Object.keys(assignments).some((id) => !exposures[id]);
|
|
247
|
+
},
|
|
248
|
+
require: (facts) => {
|
|
249
|
+
const assignments = facts.assignments as Record<string, string>;
|
|
250
|
+
const exposures = facts.exposures as Record<string, number>;
|
|
251
|
+
const experimentId = Object.keys(assignments).find(
|
|
252
|
+
(id) => !exposures[id],
|
|
253
|
+
)!;
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
type: "TRACK_EXPOSURE",
|
|
257
|
+
experimentId,
|
|
258
|
+
variantId: assignments[experimentId],
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
resolvers: {
|
|
265
|
+
assignVariant: {
|
|
266
|
+
requirement: "ASSIGN_VARIANT",
|
|
267
|
+
resolve: async (req, context) => {
|
|
268
|
+
const experiments = context.facts.experiments as Experiment[];
|
|
269
|
+
const experiment = experiments.find(
|
|
270
|
+
(e: Experiment) => e.id === req.experimentId,
|
|
271
|
+
);
|
|
272
|
+
if (!experiment) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const variantId = pickVariant(
|
|
277
|
+
context.facts.userId,
|
|
278
|
+
req.experimentId,
|
|
279
|
+
experiment.variants,
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
context.facts.assignments = {
|
|
283
|
+
...context.facts.assignments,
|
|
284
|
+
[req.experimentId]: variantId,
|
|
285
|
+
};
|
|
286
|
+
log("resolver", `Assigned ${req.experimentId} → ${variantId}`);
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
trackExposure: {
|
|
291
|
+
requirement: "TRACK_EXPOSURE",
|
|
292
|
+
resolve: async (req, context) => {
|
|
293
|
+
const now = Date.now();
|
|
294
|
+
context.facts.exposures = {
|
|
295
|
+
...context.facts.exposures,
|
|
296
|
+
[req.experimentId]: now,
|
|
297
|
+
};
|
|
298
|
+
log(
|
|
299
|
+
"resolver",
|
|
300
|
+
`Exposure tracked: ${req.experimentId} (variant: ${req.variantId}) at ${new Date(now).toLocaleTimeString()}`,
|
|
301
|
+
);
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// ============================================================================
|
|
308
|
+
// System
|
|
309
|
+
// ============================================================================
|
|
310
|
+
|
|
311
|
+
const system = createSystem({
|
|
312
|
+
module: abTesting,
|
|
313
|
+
debug: { runHistory: true },
|
|
314
|
+
plugins: [devtoolsPlugin({ name: "ab-testing" })],
|
|
315
|
+
});
|
|
316
|
+
system.start();
|
|
317
|
+
|
|
318
|
+
// ============================================================================
|
|
319
|
+
// DOM References
|
|
320
|
+
// ============================================================================
|
|
321
|
+
|
|
322
|
+
// Stats
|
|
323
|
+
|
|
324
|
+
// Timeline
|
|
325
|
+
|
|
326
|
+
// ============================================================================
|
|
327
|
+
// Render
|
|
328
|
+
// ============================================================================
|
|
329
|
+
|
|
330
|
+
function escapeHtml(text: string): string {
|
|
331
|
+
|
|
332
|
+
return div.innerHTML;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
// ============================================================================
|
|
337
|
+
// Subscribe
|
|
338
|
+
// ============================================================================
|
|
339
|
+
|
|
340
|
+
system.subscribe(
|
|
341
|
+
[
|
|
342
|
+
"experiments",
|
|
343
|
+
"assignments",
|
|
344
|
+
"exposures",
|
|
345
|
+
"userId",
|
|
346
|
+
"paused",
|
|
347
|
+
"activeExperiments",
|
|
348
|
+
"assignedCount",
|
|
349
|
+
"exposedCount",
|
|
350
|
+
],
|
|
351
|
+
render,
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
// Button handlers
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
// ============================================================================
|
|
358
|
+
// Register sample experiments
|
|
359
|
+
// ============================================================================
|
|
360
|
+
|
|
361
|
+
system.events.registerExperiment({
|
|
362
|
+
id: "theme-icons",
|
|
363
|
+
name: "Theme Icons",
|
|
364
|
+
variants: [
|
|
365
|
+
{ id: "custom-svg", weight: 50, label: "Custom SVG" },
|
|
366
|
+
{ id: "phosphor", weight: 50, label: "Phosphor" },
|
|
367
|
+
],
|
|
368
|
+
});
|
|
369
|
+
log("event", "Registered experiment: theme-icons");
|
|
370
|
+
|
|
371
|
+
system.events.registerExperiment({
|
|
372
|
+
id: "cta-color",
|
|
373
|
+
name: "CTA Button Color",
|
|
374
|
+
variants: [
|
|
375
|
+
{ id: "brand", weight: 50, label: "Brand" },
|
|
376
|
+
{ id: "green", weight: 30, label: "Green" },
|
|
377
|
+
{ id: "orange", weight: 20, label: "Orange" },
|
|
378
|
+
],
|
|
379
|
+
});
|
|
380
|
+
log("event", "Registered experiment: cta-color");
|
|
381
|
+
|
|
382
|
+
// Initial render
|
|
383
|
+
render();
|
|
384
|
+
|
|
385
|
+
// Signal to tests that initialization is complete
|