@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,341 @@
1
+ // Example: goal-heist
2
+ // Source: examples/goal-heist/src/agents.ts
3
+ // Pure module file — no DOM wiring
4
+
5
+ import { createRunner } from "@directive-run/ai";
6
+ import type { GoalNode } from "@directive-run/ai";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // API key management (localStorage)
10
+ // ---------------------------------------------------------------------------
11
+
12
+ const STORAGE_KEY = "goal-heist-api-key";
13
+
14
+ export function getApiKey(): string | null {
15
+ return localStorage.getItem(STORAGE_KEY);
16
+ }
17
+
18
+ export function setApiKey(key: string): void {
19
+ localStorage.setItem(STORAGE_KEY, key);
20
+ }
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Agent metadata
24
+ // ---------------------------------------------------------------------------
25
+
26
+ export interface HeistAgent {
27
+ id: string;
28
+ name: string;
29
+ emoji: string;
30
+ title: string;
31
+ produces: string[];
32
+ requires: string[];
33
+ instruction: string;
34
+ mockResponse: Record<string, unknown>;
35
+ mockDelay: number;
36
+ }
37
+
38
+ export const AGENTS: Record<string, HeistAgent> = {
39
+ gigi: {
40
+ id: "gigi",
41
+ name: "Gigi",
42
+ emoji: "\uD83D\uDC84",
43
+ title: "The Grifter",
44
+ produces: ["guard_schedule"],
45
+ requires: [],
46
+ instruction:
47
+ 'You are Gigi "The Grifter", a master of social engineering. You sweet-talked the night guard and obtained their patrol schedule. Respond with JSON: { "guard_schedule": "<brief schedule description>" }',
48
+ mockResponse: {
49
+ guard_schedule:
50
+ "Guards rotate every 45min. East wing unpatrolled 2:15-3:00 AM. Shift change at 3 AM — 4min blind spot.",
51
+ },
52
+ mockDelay: 800,
53
+ },
54
+ felix: {
55
+ id: "felix",
56
+ name: "Felix",
57
+ emoji: "\uD83D\uDD8A\uFE0F",
58
+ title: "The Forger",
59
+ produces: ["blueprints"],
60
+ requires: [],
61
+ instruction:
62
+ 'You are Felix "The Forger", an expert document forger. You acquired the museum floor plans from the city records archive. Respond with JSON: { "blueprints": "<brief blueprint description>" }',
63
+ mockResponse: {
64
+ blueprints:
65
+ "Floor plan secured. Vault in sub-basement B2, access via service elevator. Air ducts too narrow — main corridor only.",
66
+ },
67
+ mockDelay: 1000,
68
+ },
69
+ vince: {
70
+ id: "vince",
71
+ name: "Vince",
72
+ emoji: "\uD83D\uDE97",
73
+ title: "The Wheelman",
74
+ produces: ["escape_route"],
75
+ requires: [],
76
+ instruction:
77
+ 'You are Vince "The Wheelman", the fastest driver in the city. You scouted three escape routes and picked the best one. Respond with JSON: { "escape_route": "<brief route description>" }',
78
+ mockResponse: {
79
+ escape_route:
80
+ "Primary: loading dock → alley → I-90 on-ramp. Backup: north exit → parking garage swap. ETA to safe house: 8 minutes.",
81
+ },
82
+ mockDelay: 600,
83
+ },
84
+ h4x: {
85
+ id: "h4x",
86
+ name: "H4X",
87
+ emoji: "\uD83D\uDCBB",
88
+ title: "The Hacker",
89
+ produces: ["cameras_disabled"],
90
+ requires: ["guard_schedule"],
91
+ instruction:
92
+ 'You are H4X "The Hacker". Using the guard schedule, you found the perfect window to loop the security cameras. Respond with JSON: { "cameras_disabled": "<brief description>" }',
93
+ mockResponse: {
94
+ cameras_disabled:
95
+ "Cameras on loop from 2:15 AM. Feed shows empty corridors on repeat. Motion sensors in east wing bypassed.",
96
+ },
97
+ mockDelay: 1200,
98
+ },
99
+ luca: {
100
+ id: "luca",
101
+ name: "Luca",
102
+ emoji: "\uD83D\uDD13",
103
+ title: "The Locksmith",
104
+ produces: ["vault_cracked"],
105
+ requires: ["cameras_disabled", "blueprints"],
106
+ instruction:
107
+ 'You are Luca "The Locksmith". With cameras down and blueprints in hand, you cracked the vault. Respond with JSON: { "vault_cracked": "<brief description>" }',
108
+ mockResponse: {
109
+ vault_cracked:
110
+ "Vault open. Biometric bypass took 90 seconds. Package secured. No alarms triggered.",
111
+ },
112
+ mockDelay: 1500,
113
+ },
114
+ ollie: {
115
+ id: "ollie",
116
+ name: "Ollie",
117
+ emoji: "\uD83D\uDC41\uFE0F",
118
+ title: "The Lookout",
119
+ produces: ["all_clear"],
120
+ requires: ["vault_cracked", "escape_route"],
121
+ instruction:
122
+ 'You are Ollie "The Lookout". The vault is cracked and the escape route is ready. Confirm all clear for extraction. Respond with JSON: { "all_clear": "<brief confirmation>" }',
123
+ mockResponse: {
124
+ all_clear:
125
+ "All clear. No police activity within 2 miles. Team converging on loading dock. Go go go.",
126
+ },
127
+ mockDelay: 700,
128
+ },
129
+ };
130
+
131
+ // Ordered list for rendering
132
+ export const AGENT_ORDER = ["gigi", "felix", "vince", "h4x", "luca", "ollie"];
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // Satisfaction weights
136
+ // ---------------------------------------------------------------------------
137
+
138
+ export const WEIGHTS: Record<string, number> = {
139
+ guard_schedule: 0.1,
140
+ blueprints: 0.1,
141
+ escape_route: 0.05,
142
+ cameras_disabled: 0.2,
143
+ vault_cracked: 0.35,
144
+ all_clear: 0.2,
145
+ };
146
+
147
+ export function computeSatisfaction(facts: Record<string, unknown>): number {
148
+ let score = 0;
149
+
150
+ for (const [key, weight] of Object.entries(WEIGHTS)) {
151
+ if (facts[key] != null) {
152
+ score += weight;
153
+ }
154
+ }
155
+
156
+ return Math.min(score, 1);
157
+ }
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // Goal nodes (used by runGoal)
161
+ // ---------------------------------------------------------------------------
162
+
163
+ export function buildGoalNodes(): Record<string, GoalNode> {
164
+ const nodes: Record<string, GoalNode> = {};
165
+
166
+ for (const agent of Object.values(AGENTS)) {
167
+ nodes[agent.id] = {
168
+ agent: agent.id,
169
+ produces: agent.produces,
170
+ requires: agent.requires.length > 0 ? agent.requires : undefined,
171
+ buildInput: (facts) => {
172
+ const relevantFacts: Record<string, unknown> = {};
173
+
174
+ for (const key of agent.requires) {
175
+ if (facts[key] != null) {
176
+ relevantFacts[key] = facts[key];
177
+ }
178
+ }
179
+
180
+ return JSON.stringify(relevantFacts);
181
+ },
182
+ extractOutput: (result) => {
183
+ try {
184
+ const parsed =
185
+ typeof result.output === "string"
186
+ ? JSON.parse(result.output)
187
+ : result.output;
188
+ const extracted: Record<string, unknown> = {};
189
+
190
+ for (const key of agent.produces) {
191
+ if (parsed[key] != null) {
192
+ extracted[key] = parsed[key];
193
+ }
194
+ }
195
+
196
+ return extracted;
197
+ } catch {
198
+ return {};
199
+ }
200
+ },
201
+ };
202
+ }
203
+
204
+ return nodes;
205
+ }
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // Runner factory (real Claude or mock)
209
+ // ---------------------------------------------------------------------------
210
+
211
+ export function createHeistRunner(apiKey: string | null) {
212
+ if (apiKey) {
213
+ return createRunner({
214
+ buildRequest: (agent, input) => ({
215
+ url: "/api/claude",
216
+ init: {
217
+ method: "POST",
218
+ headers: {
219
+ "Content-Type": "application/json",
220
+ "x-api-key": apiKey,
221
+ },
222
+ body: JSON.stringify({
223
+ model: "claude-haiku-4-5-20251001",
224
+ max_tokens: 256,
225
+ system: agent.instructions ?? "",
226
+ messages: [{ role: "user", content: input }],
227
+ }),
228
+ },
229
+ }),
230
+ parseResponse: async (res) => {
231
+ const data = await res.json();
232
+ const text = data.content?.[0]?.text ?? "";
233
+ const inputTokens = data.usage?.input_tokens ?? 0;
234
+ const outputTokens = data.usage?.output_tokens ?? 0;
235
+
236
+ return {
237
+ text,
238
+ totalTokens: inputTokens + outputTokens,
239
+ };
240
+ },
241
+ parseOutput: (text) => {
242
+ try {
243
+ return JSON.parse(text);
244
+ } catch {
245
+ return text;
246
+ }
247
+ },
248
+ });
249
+ }
250
+
251
+ // Mock runner — configurable delays, supports failure injection
252
+ return createMockRunner();
253
+ }
254
+
255
+ // ---------------------------------------------------------------------------
256
+ // Mock runner with failure injection
257
+ // ---------------------------------------------------------------------------
258
+
259
+ let failHacker = false;
260
+ let failForger = false;
261
+ let hackerFailCount = 0;
262
+
263
+ export function setFailHacker(v: boolean): void {
264
+ failHacker = v;
265
+ hackerFailCount = 0;
266
+ }
267
+
268
+ export function setFailForger(v: boolean): void {
269
+ failForger = v;
270
+ }
271
+
272
+ function createMockRunner() {
273
+ return createRunner({
274
+ buildRequest: (agent, input) => ({
275
+ url: "mock://local",
276
+ init: {
277
+ method: "POST",
278
+ headers: { "Content-Type": "application/json" },
279
+ body: JSON.stringify({ agent: agent.name, input }),
280
+ },
281
+ }),
282
+ parseResponse: async (res) => {
283
+ const data = await res.json();
284
+ const text = data.content?.[0]?.text ?? "";
285
+ const tokens = data.usage?.total_tokens ?? 0;
286
+
287
+ return { text, totalTokens: tokens };
288
+ },
289
+ parseOutput: (text) => {
290
+ try {
291
+ return JSON.parse(text);
292
+ } catch {
293
+ return text;
294
+ }
295
+ },
296
+ // Mock fetch — adds delay, failure injection, returns Anthropic-shaped response
297
+ fetch: async (_url: RequestInfo | URL, init?: RequestInit) => {
298
+ const body = JSON.parse((init?.body as string) ?? "{}");
299
+ const agentName = (body.agent as string)?.toLowerCase() ?? "";
300
+
301
+ const agentDef = Object.values(AGENTS).find(
302
+ (a) => a.name.toLowerCase() === agentName,
303
+ );
304
+ const delay = agentDef?.mockDelay ?? 800;
305
+
306
+ await new Promise((resolve) => setTimeout(resolve, delay));
307
+
308
+ // Failure injection
309
+ if (agentName === "h4x" && failHacker) {
310
+ hackerFailCount++;
311
+
312
+ if (hackerFailCount <= 3) {
313
+ return new Response(
314
+ JSON.stringify({ error: "Firewall upgraded! Intrusion detected." }),
315
+ { status: 500 },
316
+ );
317
+ }
318
+ }
319
+
320
+ if (agentName === "felix" && failForger) {
321
+ return new Response(
322
+ JSON.stringify({ error: "Felix arrested at the archive!" }),
323
+ { status: 500 },
324
+ );
325
+ }
326
+
327
+ const mockResp = agentDef?.mockResponse ?? {};
328
+ const tokens = Math.floor(Math.random() * 40) + 20;
329
+
330
+ const responseBody = {
331
+ content: [{ text: JSON.stringify(mockResp) }],
332
+ usage: { total_tokens: tokens },
333
+ };
334
+
335
+ return new Response(JSON.stringify(responseBody), {
336
+ status: 200,
337
+ headers: { "Content-Type": "application/json" },
338
+ });
339
+ },
340
+ });
341
+ }
@@ -0,0 +1,57 @@
1
+ // Example: multi-module
2
+ // Source: examples/multi-module/src/main.ts
3
+ // Extracted for AI rules — DOM wiring stripped
4
+
5
+ /**
6
+ * Multi-Module Example - Main Entry Point
7
+ *
8
+ * Demonstrates the NEW namespaced module access:
9
+ * - `system.facts.auth.token` instead of `system.facts.auth_token`
10
+ * - `system.derive.data.userCount` instead of `system.derive.data_userCount`
11
+ * - `system.events.auth.login({ token })` instead of `dispatch({ type: "auth_login", token })`
12
+ *
13
+ * Cross-module constraints work automatically:
14
+ * - Data fetches when auth succeeds
15
+ * - No asCombined() helper needed
16
+ */
17
+
18
+ import { getFacts, system } from "./system";
19
+
20
+ // DOM Elements
21
+
22
+ // Start the system
23
+ system.start();
24
+
25
+ // Update UI function
26
+
27
+ // Subscribe to derivation changes using namespaced keys
28
+ // Note: The internal keys are still prefixed (auth_status), so we use those for subscribe
29
+ system.subscribe(
30
+ [
31
+ "auth_status",
32
+ "auth_displayName",
33
+ "data_status",
34
+ "data_userCount",
35
+ "ui_hasNotifications",
36
+ ],
37
+ () => {
38
+ updateUI();
39
+ },
40
+ );
41
+
42
+ // Also update on fact changes via polling (simple approach for this demo)
43
+
44
+ // Event handlers using namespaced events accessor
45
+
46
+
47
+ // Initial render
48
+ updateUI();
49
+
50
+ // Log to console for debugging
51
+ console.log("Multi-Module Example Started (Namespaced Mode)");
52
+ console.log("Try clicking Login to see the cross-module constraint in action:");
53
+ console.log("1. Auth module validates token via facts.auth.*");
54
+ console.log(
55
+ "2. Data module automatically fetches users when facts.auth.isAuthenticated",
56
+ );
57
+ console.log("3. UI module effects react to facts.data.* changes");
@@ -0,0 +1,241 @@
1
+ // Example: newsletter
2
+ // Source: examples/newsletter/src/main.ts
3
+ // Extracted for AI rules — DOM wiring stripped
4
+
5
+ /**
6
+ * Newsletter Signup - Vanilla Directive Example
7
+ *
8
+ * Demonstrates all six primitives with the simplest possible module:
9
+ * - Facts: email, touched, status, errorMessage, lastSubmittedAt
10
+ * - Derivations: emailError (touch-gated), isValid, canSubmit (rate-limited)
11
+ * - Events: updateEmail, touchEmail, submit
12
+ * - Constraints: subscribe (status === 'submitting'), resetAfterSuccess
13
+ * - Resolvers: simulated async subscribe, auto-reset after delay
14
+ * - Effects: logging status transitions
15
+ *
16
+ * Uses a simulated setTimeout instead of a real API so no account is needed.
17
+ */
18
+
19
+ import {
20
+ type ModuleSchema,
21
+ createModule,
22
+ createSystem,
23
+ t,
24
+ } from "@directive-run/core";
25
+ import { devtoolsPlugin } from "@directive-run/core/plugins";
26
+
27
+ // ============================================================================
28
+ // Constants
29
+ // ============================================================================
30
+
31
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
32
+ const RATE_LIMIT_MS = 10_000; // 10 seconds (shorter for demo)
33
+
34
+ // ============================================================================
35
+ // Schema
36
+ // ============================================================================
37
+
38
+ const schema = {
39
+ facts: {
40
+ email: t.string(),
41
+ touched: t.boolean(),
42
+ status: t.string<"idle" | "submitting" | "success" | "error">(),
43
+ errorMessage: t.string(),
44
+ lastSubmittedAt: t.number(),
45
+ },
46
+ derivations: {
47
+ emailError: t.string(),
48
+ isValid: t.boolean(),
49
+ canSubmit: t.boolean(),
50
+ },
51
+ events: {
52
+ updateEmail: { value: t.string() },
53
+ touchEmail: {},
54
+ submit: {},
55
+ },
56
+ requirements: {
57
+ SUBSCRIBE: {},
58
+ RESET_AFTER_DELAY: {},
59
+ },
60
+ } satisfies ModuleSchema;
61
+
62
+ // ============================================================================
63
+ // Module
64
+ // ============================================================================
65
+
66
+ const newsletter = createModule("newsletter", {
67
+ schema,
68
+
69
+ init: (facts) => {
70
+ facts.email = "";
71
+ facts.touched = false;
72
+ facts.status = "idle";
73
+ facts.errorMessage = "";
74
+ facts.lastSubmittedAt = 0;
75
+ },
76
+
77
+ derive: {
78
+ emailError: (facts) => {
79
+ if (!facts.touched) {
80
+ return "";
81
+ }
82
+ if (!facts.email.trim()) {
83
+ return "Email is required";
84
+ }
85
+ if (!EMAIL_REGEX.test(facts.email)) {
86
+ return "Enter a valid email address";
87
+ }
88
+
89
+ return "";
90
+ },
91
+
92
+ isValid: (facts) => EMAIL_REGEX.test(facts.email),
93
+
94
+ canSubmit: (facts, derive) => {
95
+ if (!derive.isValid) {
96
+ return false;
97
+ }
98
+ if (facts.status !== "idle") {
99
+ return false;
100
+ }
101
+ if (
102
+ facts.lastSubmittedAt > 0 &&
103
+ Date.now() - facts.lastSubmittedAt < RATE_LIMIT_MS
104
+ ) {
105
+ return false;
106
+ }
107
+
108
+ return true;
109
+ },
110
+ },
111
+
112
+ events: {
113
+ updateEmail: (facts, { value }) => {
114
+ facts.email = value;
115
+ },
116
+
117
+ touchEmail: (facts) => {
118
+ facts.touched = true;
119
+ },
120
+
121
+ submit: (facts) => {
122
+ facts.touched = true;
123
+ facts.status = "submitting";
124
+ },
125
+ },
126
+
127
+ constraints: {
128
+ subscribe: {
129
+ when: (facts) => facts.status === "submitting",
130
+ require: { type: "SUBSCRIBE" },
131
+ },
132
+
133
+ resetAfterSuccess: {
134
+ when: (facts) => facts.status === "success",
135
+ require: { type: "RESET_AFTER_DELAY" },
136
+ },
137
+ },
138
+
139
+ resolvers: {
140
+ // Simulated submission — no API account needed
141
+ subscribe: {
142
+ requirement: "SUBSCRIBE",
143
+ resolve: async (req, context) => {
144
+ log(`Subscribing: ${context.facts.email}`);
145
+
146
+ // Simulate network delay
147
+ await new Promise((resolve) => setTimeout(resolve, 1500));
148
+
149
+ // Simulate occasional failure (20% chance)
150
+ if (Math.random() < 0.2) {
151
+ context.facts.status = "error";
152
+ context.facts.errorMessage =
153
+ "Simulated error — try again (20% failure rate for demo).";
154
+ log("Subscription failed (simulated)");
155
+
156
+ return;
157
+ }
158
+
159
+ context.facts.status = "success";
160
+ context.facts.lastSubmittedAt = Date.now();
161
+ log("Subscription succeeded");
162
+ },
163
+ },
164
+
165
+ resetAfterDelay: {
166
+ requirement: "RESET_AFTER_DELAY",
167
+ resolve: async (req, context) => {
168
+ log("Auto-resetting in 5 seconds...");
169
+ await new Promise((resolve) => setTimeout(resolve, 5000));
170
+ context.facts.email = "";
171
+ context.facts.touched = false;
172
+ context.facts.status = "idle";
173
+ context.facts.errorMessage = "";
174
+ log("Form reset");
175
+ },
176
+ },
177
+ },
178
+
179
+ effects: {
180
+ logSubscription: {
181
+ deps: ["status"],
182
+ run: (facts, prev) => {
183
+ if (!prev) {
184
+ return;
185
+ }
186
+
187
+ if (facts.status !== prev.status) {
188
+ log(`Status: ${prev.status} → ${facts.status}`);
189
+ }
190
+ },
191
+ },
192
+ },
193
+ });
194
+
195
+ // ============================================================================
196
+ // System
197
+ // ============================================================================
198
+
199
+ const system = createSystem({
200
+ module: newsletter,
201
+ debug: { runHistory: true },
202
+ plugins: [devtoolsPlugin({ name: "newsletter" })],
203
+ });
204
+ system.start();
205
+
206
+ // ============================================================================
207
+ // Logging helper
208
+ // ============================================================================
209
+
210
+ function log(msg: string) {
211
+ console.log(`[newsletter] ${msg}`);
212
+ }
213
+
214
+ // ============================================================================
215
+ // DOM Bindings
216
+ // ============================================================================
217
+
218
+
219
+ // ============================================================================
220
+ // Render
221
+ // ============================================================================
222
+
223
+
224
+ // Subscribe to all relevant facts and derivations
225
+ system.subscribe(
226
+ [
227
+ "email",
228
+ "touched",
229
+ "status",
230
+ "errorMessage",
231
+ "lastSubmittedAt",
232
+ "emailError",
233
+ "isValid",
234
+ "canSubmit",
235
+ ],
236
+ render,
237
+ );
238
+
239
+ // Initial render
240
+ render();
241
+ log("Newsletter signup ready. Enter an email and subscribe.");