# Examples

> Auto-generated from extracted examples. Do not edit manually.

## counter

```typescript
// Example: counter
// Source: examples/counter/src/module.ts
// Pure module file — no DOM wiring

/**
 * Counter — The simplest Directive module.
 *
 * Demonstrates: facts, events, derivations, one constraint, one resolver.
 * Total: ~40 lines.
 */

import {
  type ModuleSchema,
  createModule,
  createSystem,
  t,
} from "@directive-run/core";

const schema = {
  facts: {
    count: t.number(),
  },
  derivations: {
    doubled: t.number(),
    isPositive: t.boolean(),
  },
  events: {
    increment: {},
    decrement: {},
    reset: {},
  },
  requirements: {
    CLAMP_TO_ZERO: {},
  },
} satisfies ModuleSchema;

export const counterModule = createModule("counter", {
  schema,

  init: (facts) => {
    facts.count = 0;
  },

  derive: {
    doubled: (facts) => facts.count * 2,
    isPositive: (facts) => facts.count > 0,
  },

  events: {
    increment: (facts) => {
      facts.count += 1;
    },
    decrement: (facts) => {
      facts.count -= 1;
    },
    reset: (facts) => {
      facts.count = 0;
    },
  },

  // When count goes negative, automatically fix it
  constraints: {
    noNegative: {
      when: (facts) => facts.count < 0,
      require: { type: "CLAMP_TO_ZERO" },
    },
  },

  resolvers: {
    clamp: {
      requirement: "CLAMP_TO_ZERO",
      resolve: async (_req, context) => {
        context.facts.count = 0;
      },
    },
  },
});

export const system = createSystem({ module: counterModule });
```

## contact-form

```typescript
// Example: contact-form
// Source: examples/contact-form/src/module.ts
// Pure module file — no DOM wiring

/**
 * Contact Form — Directive Module
 *
 * Multi-field contact form showcasing validation, constraints, and resolvers:
 * - Facts: name, email, subject, message, touched, status, errorMessage, etc.
 * - Derivations: field errors, isValid, canSubmit, messageCharCount
 * - Events: updateField, touchField, submit, reset
 * - Constraints: submitForm, resetAfterSuccess
 * - Resolvers: simulated async send, auto-reset after delay
 * - Effects: logging status transitions
 *
 * Uses a simulated setTimeout instead of a real API so no account is needed.
 */

import {
  type ModuleSchema,
  createModule,
  createSystem,
  t,
} from "@directive-run/core";
import { devtoolsPlugin } from "@directive-run/core/plugins";

// ============================================================================
// Constants
// ============================================================================

const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const RATE_LIMIT_MS = 10_000; // 10 seconds (shorter for demo)

// ============================================================================
// Types
// ============================================================================

export interface TimelineEntry {
  time: number;
  event: string;
  detail: string;
  type: string;
}

// ============================================================================
// Timeline (external mutable array, same pattern as fraud-analysis)
// ============================================================================

export const timeline: TimelineEntry[] = [];

export function addTimelineEntry(
  event: string,
  detail: string,
  type: string,
): void {
  timeline.unshift({ time: Date.now(), event, detail, type });
}

// ============================================================================
// Logs helper
// ============================================================================

export function log(msg: string): void {
  console.log(`[contact-form] ${msg}`);

  // Classify and add to timeline
  if (msg.startsWith("Sending:")) {
    addTimelineEntry("submit", msg.replace("Sending: ", ""), "submit");
  } else if (msg.includes("succeeded")) {
    addTimelineEntry("success", msg, "submit");
  } else if (msg.includes("failed")) {
    addTimelineEntry("error", msg, "error");
  } else if (msg.startsWith("Status:")) {
    addTimelineEntry("status", msg.replace("Status: ", ""), "field");
  } else if (msg.includes("Auto-resetting")) {
    addTimelineEntry("auto-reset", msg, "reset");
  } else if (msg === "Form reset") {
    addTimelineEntry("reset", "Form cleared", "reset");
  } else if (msg.includes("ready")) {
    addTimelineEntry("init", msg, "field");
  }
}

// ============================================================================
// Schema
// ============================================================================

export const schema = {
  facts: {
    name: t.string(),
    email: t.string(),
    subject: t.string(),
    message: t.string(),
    touched: t.object<Record<string, boolean>>(),
    status: t.string<"idle" | "submitting" | "success" | "error">(),
    errorMessage: t.string(),
    lastSubmittedAt: t.number(),
    submissionCount: t.number(),
  },
  derivations: {
    nameError: t.string(),
    emailError: t.string(),
    subjectError: t.string(),
    messageError: t.string(),
    isValid: t.boolean(),
    canSubmit: t.boolean(),
    messageCharCount: t.number(),
  },
  events: {
    updateField: { field: t.string(), value: t.string() },
    touchField: { field: t.string() },
    submit: {},
    reset: {},
  },
  requirements: {
    SEND_MESSAGE: {},
    RESET_AFTER_DELAY: {},
  },
} satisfies ModuleSchema;

// ============================================================================
// Module
// ============================================================================

const contactForm = createModule("contact-form", {
  schema,

  init: (facts) => {
    facts.name = "";
    facts.email = "";
    facts.subject = "";
    facts.message = "";
    facts.touched = {};
    facts.status = "idle";
    facts.errorMessage = "";
    facts.lastSubmittedAt = 0;
    facts.submissionCount = 0;
  },

  derive: {
    nameError: (facts) => {
      if (!facts.touched.name) {
        return "";
      }
      if (!facts.name.trim()) {
        return "Name is required";
      }
      if (facts.name.trim().length < 2) {
        return "Name must be at least 2 characters";
      }

      return "";
    },

    emailError: (facts) => {
      if (!facts.touched.email) {
        return "";
      }
      if (!facts.email.trim()) {
        return "Email is required";
      }
      if (!EMAIL_REGEX.test(facts.email)) {
        return "Enter a valid email address";
      }

      return "";
    },

    subjectError: (facts) => {
      if (!facts.touched.subject) {
        return "";
      }
      if (!facts.subject) {
        return "Please select a subject";
      }

      return "";
    },

    messageError: (facts) => {
      if (!facts.touched.message) {
        return "";
      }
      if (!facts.message.trim()) {
        return "Message is required";
      }
      if (facts.message.trim().length < 10) {
        return "Message must be at least 10 characters";
      }

      return "";
    },

    isValid: (facts) =>
      facts.name.trim().length >= 2 &&
      EMAIL_REGEX.test(facts.email) &&
      facts.subject !== "" &&
      facts.message.trim().length >= 10,

    canSubmit: (facts, derived) => {
      if (!derived.isValid) {
        return false;
      }
      if (facts.status !== "idle") {
        return false;
      }
      if (
        facts.lastSubmittedAt > 0 &&
        Date.now() - facts.lastSubmittedAt < RATE_LIMIT_MS
      ) {
        return false;
      }

      return true;
    },

    messageCharCount: (facts) => facts.message.length,
  },

  events: {
    updateField: (facts, { field, value }) => {
      const key = field as "name" | "email" | "subject" | "message";
      if (key in facts && typeof facts[key] === "string") {
        (facts as unknown as Record<string, string>)[key] = value;
      }
    },

    touchField: (facts, { field }) => {
      facts.touched = { ...facts.touched, [field]: true };
    },

    submit: (facts) => {
      facts.touched = { name: true, email: true, subject: true, message: true };
      facts.status = "submitting";
    },

    reset: (facts) => {
      facts.name = "";
      facts.email = "";
      facts.subject = "";
      facts.message = "";
      facts.touched = {};
      facts.status = "idle";
      facts.errorMessage = "";
    },
  },

  constraints: {
    submitForm: {
      when: (facts) => facts.status === "submitting",
      require: { type: "SEND_MESSAGE" },
    },

    resetAfterSuccess: {
      when: (facts) => facts.status === "success",
      require: { type: "RESET_AFTER_DELAY" },
    },
  },

  resolvers: {
    sendMessage: {
      requirement: "SEND_MESSAGE",
      resolve: async (_req, context) => {
        log(
          `Sending: ${context.facts.name} <${context.facts.email}> [${context.facts.subject}]`,
        );

        await new Promise((resolve) => setTimeout(resolve, 1500));

        if (Math.random() < 0.2) {
          context.facts.status = "error";
          context.facts.errorMessage =
            "Simulated error — try again (20% failure rate for demo).";
          log("Submission failed (simulated)");

          return;
        }

        context.facts.status = "success";
        context.facts.lastSubmittedAt = Date.now();
        context.facts.submissionCount++;
        log(`Submission #${context.facts.submissionCount} succeeded`);
      },
    },

    resetAfterDelay: {
      requirement: "RESET_AFTER_DELAY",
      resolve: async (_req, context) => {
        log("Auto-resetting in 3 seconds...");
        await new Promise((resolve) => setTimeout(resolve, 3000));
        context.facts.name = "";
        context.facts.email = "";
        context.facts.subject = "";
        context.facts.message = "";
        context.facts.touched = {};
        context.facts.status = "idle";
        context.facts.errorMessage = "";
        log("Form reset");
      },
    },
  },

  effects: {
    logSubmission: {
      deps: ["status", "submissionCount"],
      run: (facts, prevFacts) => {
        if (!prevFacts) {
          return;
        }

        if (facts.status !== prevFacts.status) {
          log(`Status: ${prevFacts.status} → ${facts.status}`);
        }
      },
    },
  },
});

// ============================================================================
// System
// ============================================================================

export const system = createSystem({
  module: contactForm,
  trace: true,
  plugins: [devtoolsPlugin({ name: "contact-form" })],
});
```

## newsletter

```typescript
// Example: newsletter
// Source: examples/newsletter/src/module.ts
// Pure module file — no DOM wiring

/**
 * Newsletter Signup — Directive Module
 *
 * Demonstrates all six primitives with the simplest possible module:
 * - Facts: email, touched, status, errorMessage, lastSubmittedAt
 * - Derivations: emailError (touch-gated), isValid, canSubmit (rate-limited)
 * - Events: updateEmail, touchEmail, submit
 * - Constraints: subscribe (status === 'submitting'), resetAfterSuccess
 * - Resolvers: simulated async subscribe, auto-reset after delay
 * - Effects: logging status transitions
 *
 * Uses a simulated setTimeout instead of a real API so no account is needed.
 */

import {
  type ModuleSchema,
  createModule,
  createSystem,
  t,
} from "@directive-run/core";
import { devtoolsPlugin } from "@directive-run/core/plugins";

// ============================================================================
// Constants
// ============================================================================

const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const RATE_LIMIT_MS = 10_000; // 10 seconds (shorter for demo)

// ============================================================================
// Logs (external mutable array, same pattern as fraud-analysis)
// ============================================================================

export const logs: string[] = [];

export function addLog(msg: string): void {
  console.log(`[newsletter] ${msg}`);
  logs.push(`${new Date().toLocaleTimeString()}: ${msg}`);
}

// ============================================================================
// Schema
// ============================================================================

export const schema = {
  facts: {
    email: t.string(),
    touched: t.boolean(),
    status: t.string<"idle" | "submitting" | "success" | "error">(),
    errorMessage: t.string(),
    lastSubmittedAt: t.number(),
  },
  derivations: {
    emailError: t.string(),
    isValid: t.boolean(),
    canSubmit: t.boolean(),
  },
  events: {
    updateEmail: { value: t.string() },
    touchEmail: {},
    submit: {},
  },
  requirements: {
    SUBSCRIBE: {},
    RESET_AFTER_DELAY: {},
  },
} satisfies ModuleSchema;

// ============================================================================
// Module
// ============================================================================

const newsletter = createModule("newsletter", {
  schema,

  init: (facts) => {
    facts.email = "";
    facts.touched = false;
    facts.status = "idle";
    facts.errorMessage = "";
    facts.lastSubmittedAt = 0;
  },

  derive: {
    emailError: (facts) => {
      if (!facts.touched) {
        return "";
      }
      if (!facts.email.trim()) {
        return "Email is required";
      }
      if (!EMAIL_REGEX.test(facts.email)) {
        return "Enter a valid email address";
      }

      return "";
    },

    isValid: (facts) => EMAIL_REGEX.test(facts.email),

    canSubmit: (facts, derived) => {
      if (!derived.isValid) {
        return false;
      }
      if (facts.status !== "idle") {
        return false;
      }
      if (
        facts.lastSubmittedAt > 0 &&
        Date.now() - facts.lastSubmittedAt < RATE_LIMIT_MS
      ) {
        return false;
      }

      return true;
    },
  },

  events: {
    updateEmail: (facts, { value }) => {
      facts.email = value;
    },

    touchEmail: (facts) => {
      facts.touched = true;
    },

    submit: (facts) => {
      facts.touched = true;
      facts.status = "submitting";
    },
  },

  constraints: {
    subscribe: {
      when: (facts) => facts.status === "submitting",
      require: { type: "SUBSCRIBE" },
    },

    resetAfterSuccess: {
      when: (facts) => facts.status === "success",
      require: { type: "RESET_AFTER_DELAY" },
    },
  },

  resolvers: {
    // Simulated submission — no API account needed
    subscribe: {
      requirement: "SUBSCRIBE",
      resolve: async (_req, context) => {
        addLog(`Subscribing: ${context.facts.email}`);

        // Simulate network delay
        await new Promise((resolve) => setTimeout(resolve, 1500));

        // Simulate occasional failure (20% chance)
        if (Math.random() < 0.2) {
          context.facts.status = "error";
          context.facts.errorMessage =
            "Simulated error — try again (20% failure rate for demo).";
          addLog("Subscription failed (simulated)");

          return;
        }

        context.facts.status = "success";
        context.facts.lastSubmittedAt = Date.now();
        addLog("Subscription succeeded");
      },
    },

    resetAfterDelay: {
      requirement: "RESET_AFTER_DELAY",
      resolve: async (_req, context) => {
        addLog("Auto-resetting in 5 seconds...");
        await new Promise((resolve) => setTimeout(resolve, 5000));
        context.facts.email = "";
        context.facts.touched = false;
        context.facts.status = "idle";
        context.facts.errorMessage = "";
        addLog("Form reset");
      },
    },
  },

  effects: {
    logSubscription: {
      deps: ["status"],
      run: (facts, prevFacts) => {
        if (!prevFacts) {
          return;
        }

        if (facts.status !== prevFacts.status) {
          addLog(`Status: ${prevFacts.status} → ${facts.status}`);
        }
      },
    },
  },
});

// ============================================================================
// System
// ============================================================================

export const system = createSystem({
  module: newsletter,
  trace: true,
  plugins: [devtoolsPlugin({ name: "newsletter" })],
});
```

## feature-flags

```typescript
// Example: feature-flags
// Source: examples/feature-flags/src/module.ts
// Pure module file — no DOM wiring

/**
 * Feature Flags Directive Module (Example)
 *
 * Mirrors the real feature flag system running on directive.run.
 * 8 flags with two interaction patterns:
 *
 * 1. Maintenance mode &ndash; disables chat, search, playground, and vote API
 * 2. Onboarding toast &rarr; depends on brand switcher (constraint auto-enables)
 */
import { createModule, t } from "@directive-run/core";

export const featureFlagsModule = createModule("feature-flags", {
  schema: {
    facts: {
      // Individual feature toggles
      chatEnabled: t.boolean(),
      searchEnabled: t.boolean(),
      playgroundEnabled: t.boolean(),
      brandSwitcherEnabled: t.boolean(),
      themeSelectorEnabled: t.boolean(),
      onboardingToastEnabled: t.boolean(),
      versionSelectorEnabled: t.boolean(),
      voteApiEnabled: t.boolean(),

      // Context
      maintenanceMode: t.boolean(),
    },
    derivations: {
      canUseChat: t.boolean(),
      canUseSearch: t.boolean(),
      canUsePlayground: t.boolean(),
      canUseBrandSwitcher: t.boolean(),
      canUseThemeSelector: t.boolean(),
      canShowOnboardingToast: t.boolean(),
      canUseVersionSelector: t.boolean(),
      canUseVoteApi: t.boolean(),
      enabledCount: t.number(),
      allFeaturesEnabled: t.boolean(),
    },
    events: {
      configure: {
        chatEnabled: t.boolean(),
        searchEnabled: t.boolean(),
        playgroundEnabled: t.boolean(),
        brandSwitcherEnabled: t.boolean(),
        themeSelectorEnabled: t.boolean(),
        onboardingToastEnabled: t.boolean(),
        versionSelectorEnabled: t.boolean(),
        voteApiEnabled: t.boolean(),
      },
      setMaintenanceMode: { enabled: t.boolean() },
      toggleFlag: { flag: t.string(), enabled: t.boolean() },
      resetAll: {},
    },
    requirements: {
      ENABLE_BRAND_SWITCHER: {},
      LOG_MAINTENANCE_WARNING: {},
    },
  },

  init: (facts) => {
    facts.chatEnabled = true;
    facts.searchEnabled = true;
    facts.playgroundEnabled = true;
    facts.brandSwitcherEnabled = true;
    facts.themeSelectorEnabled = true;
    facts.onboardingToastEnabled = true;
    facts.versionSelectorEnabled = true;
    facts.voteApiEnabled = true;

    facts.maintenanceMode = false;
  },

  derive: {
    canUseChat: (facts) => facts.chatEnabled && !facts.maintenanceMode,
    canUseSearch: (facts) => facts.searchEnabled && !facts.maintenanceMode,
    canUsePlayground: (facts) =>
      facts.playgroundEnabled && !facts.maintenanceMode,
    canUseBrandSwitcher: (facts) => facts.brandSwitcherEnabled,
    canUseThemeSelector: (facts) => facts.themeSelectorEnabled,
    canShowOnboardingToast: (facts) =>
      facts.onboardingToastEnabled && facts.brandSwitcherEnabled,
    canUseVersionSelector: (facts) => facts.versionSelectorEnabled,
    canUseVoteApi: (facts) => facts.voteApiEnabled && !facts.maintenanceMode,
    enabledCount: (facts) => {
      let count = 0;
      if (facts.chatEnabled) count++;
      if (facts.searchEnabled) count++;
      if (facts.playgroundEnabled) count++;
      if (facts.brandSwitcherEnabled) count++;
      if (facts.themeSelectorEnabled) count++;
      if (facts.onboardingToastEnabled) count++;
      if (facts.versionSelectorEnabled) count++;
      if (facts.voteApiEnabled) count++;

      return count;
    },
    allFeaturesEnabled: (facts) =>
      facts.chatEnabled &&
      facts.searchEnabled &&
      facts.playgroundEnabled &&
      facts.brandSwitcherEnabled &&
      facts.themeSelectorEnabled &&
      facts.onboardingToastEnabled &&
      facts.versionSelectorEnabled &&
      facts.voteApiEnabled,
  },

  events: {
    configure: (facts, payload) => {
      facts.chatEnabled = payload.chatEnabled;
      facts.searchEnabled = payload.searchEnabled;
      facts.playgroundEnabled = payload.playgroundEnabled;
      facts.brandSwitcherEnabled = payload.brandSwitcherEnabled;
      facts.themeSelectorEnabled = payload.themeSelectorEnabled;
      facts.onboardingToastEnabled = payload.onboardingToastEnabled;
      facts.versionSelectorEnabled = payload.versionSelectorEnabled;
      facts.voteApiEnabled = payload.voteApiEnabled;
    },

    setMaintenanceMode: (facts, { enabled }) => {
      facts.maintenanceMode = enabled;
    },

    toggleFlag: (facts, { flag, enabled }) => {
      const key = flag as keyof typeof facts;
      if (key in facts && typeof facts[key] === "boolean") {
        (facts as Record<string, boolean>)[key] = enabled;
      }
    },

    resetAll: (facts) => {
      facts.chatEnabled = true;
      facts.searchEnabled = true;
      facts.playgroundEnabled = true;
      facts.brandSwitcherEnabled = true;
      facts.themeSelectorEnabled = true;
      facts.onboardingToastEnabled = true;
      facts.versionSelectorEnabled = true;
      facts.voteApiEnabled = true;
      facts.maintenanceMode = false;
    },
  },

  constraints: {
    onboardingRequiresBrandSwitcher: {
      when: (facts) =>
        facts.onboardingToastEnabled && !facts.brandSwitcherEnabled,
      require: { type: "ENABLE_BRAND_SWITCHER" },
    },

    maintenanceWarning: {
      when: (facts) => facts.maintenanceMode,
      require: { type: "LOG_MAINTENANCE_WARNING" },
    },
  },

  resolvers: {
    enableBrandSwitcher: {
      requirement: "ENABLE_BRAND_SWITCHER",
      resolve: async (_req, context) => {
        context.facts.brandSwitcherEnabled = true;
      },
    },

    logMaintenanceWarning: {
      requirement: "LOG_MAINTENANCE_WARNING",
      resolve: async (_req, _context) => {
        console.warn(
          "[feature-flags] Maintenance mode is active. Chat, search, playground, and vote API are disabled.",
        );
      },
    },
  },

  effects: {
    logChanges: {
      deps: [
        "chatEnabled",
        "searchEnabled",
        "playgroundEnabled",
        "brandSwitcherEnabled",
        "themeSelectorEnabled",
        "onboardingToastEnabled",
        "versionSelectorEnabled",
        "voteApiEnabled",
        "maintenanceMode",
      ],
      run: (facts, prevFacts) => {
        if (!prevFacts) {
          return;
        }

        const flags = [
          "chatEnabled",
          "searchEnabled",
          "playgroundEnabled",
          "brandSwitcherEnabled",
          "themeSelectorEnabled",
          "onboardingToastEnabled",
          "versionSelectorEnabled",
          "voteApiEnabled",
          "maintenanceMode",
        ] as const;

        for (const flag of flags) {
          if (facts[flag] !== prevFacts[flag]) {
            console.log(
              `[feature-flags] ${flag}: ${prevFacts[flag]} → ${facts[flag]}`,
            );
          }
        }
      },
    },
  },
});
```

## shopping-cart

```typescript
// Example: shopping-cart
// Source: examples/shopping-cart/src/shopping-cart.ts
// Pure module file — no DOM wiring

/**
 * Shopping Cart — Directive Modules
 *
 * Two modules:
 * - cart: Items, coupons, checkout with cross-module auth dependency
 * - auth: Simple authentication toggle for demo purposes
 *
 * Demonstrates cross-module constraints (`crossModuleDeps`),
 * constraint ordering (`after`), priority-based resolution,
 * and retry with exponential backoff.
 */

import {
  type ModuleSchema,
  createModule,
  createSystem,
  t,
} from "@directive-run/core";
import { devtoolsPlugin } from "@directive-run/core/plugins";
import { processCheckout, validateCoupon } from "./mock-api.js";

// ============================================================================
// Types
// ============================================================================

export interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
  maxStock: number;
  image: string;
}

// ============================================================================
// Auth Module
// ============================================================================

export const authSchema = {
  facts: {
    isAuthenticated: t.boolean(),
    userName: t.string(),
  },
  derivations: {
    isAuthenticated: t.boolean(),
  },
  events: {
    toggleAuth: {},
  },
  requirements: {},
} satisfies ModuleSchema;

export const authModule = createModule("auth", {
  schema: authSchema,

  init: (facts) => {
    facts.isAuthenticated = true;
    facts.userName = "Demo User";
  },

  derive: {
    isAuthenticated: (facts) => facts.isAuthenticated,
  },

  events: {
    toggleAuth: (facts) => {
      facts.isAuthenticated = !facts.isAuthenticated;
      if (!facts.isAuthenticated) {
        facts.userName = "";
      } else {
        facts.userName = "Demo User";
      }
    },
  },
});

// ============================================================================
// Cart Module
// ============================================================================

export const cartSchema = {
  facts: {
    items: t.array<CartItem>(),
    couponCode: t.string(),
    couponDiscount: t.number(),
    couponStatus: t.string<"idle" | "checking" | "valid" | "invalid">(),
    checkoutRequested: t.boolean(),
    checkoutStatus: t.string<"idle" | "processing" | "complete" | "failed">(),
    checkoutError: t.string(),
  },
  derivations: {
    subtotal: t.number(),
    itemCount: t.number(),
    isEmpty: t.boolean(),
    discount: t.number(),
    tax: t.number(),
    total: t.number(),
    hasOverstockedItem: t.boolean(),
    freeShipping: t.boolean(),
  },
  events: {
    addItem: {
      id: t.string(),
      name: t.string(),
      price: t.number(),
      maxStock: t.number(),
      image: t.string(),
    },
    removeItem: { id: t.string() },
    updateQuantity: { id: t.string(), quantity: t.number() },
    applyCoupon: { code: t.string() },
    clearCoupon: {},
    requestCheckout: {},
    resetCheckout: {},
  },
  requirements: {
    ADJUST_QUANTITY: {},
    VALIDATE_COUPON: { code: t.string() },
    PROCESS_CHECKOUT: {},
  },
} satisfies ModuleSchema;

export const cartModule = createModule("cart", {
  schema: cartSchema,

  crossModuleDeps: { auth: authSchema },

  init: (facts) => {
    facts.items = [
      {
        id: "headphones-1",
        name: "Wireless Headphones",
        price: 79.99,
        quantity: 1,
        maxStock: 5,
        image: "headphones",
      },
      {
        id: "keyboard-1",
        name: "Mechanical Keyboard",
        price: 129.99,
        quantity: 1,
        maxStock: 3,
        image: "keyboard",
      },
      {
        id: "hub-1",
        name: "USB-C Hub",
        price: 49.99,
        quantity: 2,
        maxStock: 10,
        image: "hub",
      },
    ];
    facts.couponCode = "";
    facts.couponDiscount = 0;
    facts.couponStatus = "idle";
    facts.checkoutRequested = false;
    facts.checkoutStatus = "idle";
    facts.checkoutError = "";
  },

  // ============================================================================
  // Derivations
  // ============================================================================

  derive: {
    subtotal: (facts) => {
      return facts.self.items.reduce(
        (sum: number, item: CartItem) => sum + item.price * item.quantity,
        0,
      );
    },

    itemCount: (facts) => {
      return facts.self.items.reduce(
        (sum: number, item: CartItem) => sum + item.quantity,
        0,
      );
    },

    isEmpty: (facts) => {
      return facts.self.items.length === 0;
    },

    discount: (facts, derived) => {
      const sub = derived.subtotal;

      return sub * (facts.self.couponDiscount / 100);
    },

    tax: (_facts, derived) => {
      const sub = derived.subtotal;
      const disc = derived.discount;

      return (sub - disc) * 0.08;
    },

    total: (_facts, derived) => {
      const sub = derived.subtotal;
      const disc = derived.discount;
      const tx = derived.tax;

      return sub - disc + tx;
    },

    hasOverstockedItem: (facts) => {
      return facts.self.items.some(
        (item: CartItem) => item.quantity > item.maxStock,
      );
    },

    freeShipping: (_facts, derived) => {
      const sub = derived.subtotal;

      return sub >= 75;
    },
  },

  // ============================================================================
  // Events
  // ============================================================================

  events: {
    addItem: (facts, { id, name, price, maxStock, image }) => {
      const existing = facts.items.find((item: CartItem) => item.id === id);
      if (existing) {
        facts.items = facts.items.map((item: CartItem) =>
          item.id === id
            ? { ...item, quantity: Math.min(item.quantity + 1, item.maxStock) }
            : item,
        );
      } else {
        facts.items = [
          ...facts.items,
          { id, name, price, quantity: 1, maxStock, image },
        ];
      }
    },

    removeItem: (facts, { id }) => {
      facts.items = facts.items.filter((item: CartItem) => item.id !== id);
    },

    updateQuantity: (facts, { id, quantity }) => {
      if (quantity <= 0) {
        facts.items = facts.items.filter((item: CartItem) => item.id !== id);

        return;
      }

      facts.items = facts.items.map((item: CartItem) =>
        item.id === id ? { ...item, quantity } : item,
      );
    },

    applyCoupon: (facts, { code }) => {
      facts.couponCode = code;
      facts.couponStatus = "idle";
      facts.couponDiscount = 0;
    },

    clearCoupon: (facts) => {
      facts.couponCode = "";
      facts.couponDiscount = 0;
      facts.couponStatus = "idle";
    },

    requestCheckout: (facts) => {
      facts.checkoutRequested = true;
      facts.checkoutStatus = "idle";
      facts.checkoutError = "";
    },

    resetCheckout: (facts) => {
      facts.checkoutRequested = false;
      facts.checkoutStatus = "idle";
      facts.checkoutError = "";
    },
  },

  // ============================================================================
  // Constraints
  // ============================================================================

  constraints: {
    quantityLimit: {
      priority: 80,
      when: (facts) => {
        const hasOverstocked = facts.self.items.some(
          (item: CartItem) => item.quantity > item.maxStock,
        );

        return hasOverstocked;
      },
      require: { type: "ADJUST_QUANTITY" },
    },

    couponValidation: {
      priority: 70,
      when: (facts) => {
        return (
          facts.self.couponCode !== "" && facts.self.couponStatus === "idle"
        );
      },
      require: (facts) => ({
        type: "VALIDATE_COUPON",
        code: facts.self.couponCode,
      }),
    },

    checkoutReady: {
      priority: 60,
      after: ["quantityLimit", "couponValidation"],
      when: (facts) => {
        const items = facts.self.items;
        const notEmpty = items.length > 0;
        const noOverstock = !items.some(
          (item: CartItem) => item.quantity > item.maxStock,
        );

        return (
          facts.self.checkoutRequested === true &&
          notEmpty &&
          noOverstock &&
          facts.auth.isAuthenticated === true
        );
      },
      require: { type: "PROCESS_CHECKOUT" },
    },
  },

  // ============================================================================
  // Resolvers
  // ============================================================================

  resolvers: {
    adjustQuantity: {
      requirement: "ADJUST_QUANTITY",
      resolve: async (_req, context) => {
        context.facts.items = context.facts.items.map((item: CartItem) => {
          if (item.quantity > item.maxStock) {
            return { ...item, quantity: item.maxStock };
          }

          return item;
        });
      },
    },

    validateCoupon: {
      requirement: "VALIDATE_COUPON",
      key: (req) => `coupon-${req.code}`,
      resolve: async (req, context) => {
        context.facts.couponStatus = "checking";

        const result = await validateCoupon(req.code);

        if (result.valid) {
          context.facts.couponDiscount = result.discount;
          context.facts.couponStatus = "valid";
        } else {
          context.facts.couponDiscount = 0;
          context.facts.couponStatus = "invalid";
        }
      },
    },

    processCheckout: {
      requirement: "PROCESS_CHECKOUT",
      retry: { attempts: 2, backoff: "exponential" },
      resolve: async (_req, context) => {
        context.facts.checkoutStatus = "processing";

        try {
          const items = context.facts.items.map((item: CartItem) => ({
            id: item.id,
            name: item.name,
            quantity: item.quantity,
            price: item.price,
          }));

          await processCheckout(items, context.facts.couponCode);

          context.facts.checkoutStatus = "complete";
          context.facts.items = [];
          context.facts.couponCode = "";
          context.facts.couponDiscount = 0;
          context.facts.couponStatus = "idle";
          context.facts.checkoutRequested = false;
        } catch (err) {
          const msg = err instanceof Error ? err.message : "Checkout failed";
          context.facts.checkoutStatus = "failed";
          context.facts.checkoutError = msg;
          context.facts.checkoutRequested = false;
          throw err;
        }
      },
    },
  },
});

// ============================================================================
// System
// ============================================================================

export const system = createSystem({
  modules: {
    cart: cartModule,
    auth: authModule,
  },
  plugins: [devtoolsPlugin({ name: "shopping-cart", panel: true })],
  history: { maxSnapshots: 50 },
  trace: true,
});
```

## form-wizard

```typescript
// Example: form-wizard
// Source: examples/form-wizard/src/form-wizard.ts
// Pure module file — no DOM wiring

/**
 * Form Wizard — Directive Modules
 *
 * Two-module system demonstrating multi-step form validation,
 * constraint-driven step advancement, cross-module async email
 * availability checking, and persistence of draft data.
 *
 * - wizard module: step navigation, field data, derivations for per-step
 *   validity, constraints to advance/submit, resolvers for step transitions.
 * - validation module: cross-module email availability check using
 *   crossModuleDeps on the wizard schema.
 */

import {
  type ModuleSchema,
  createModule,
  createSystem,
  t,
} from "@directive-run/core";
import { devtoolsPlugin, persistencePlugin } from "@directive-run/core/plugins";

// ============================================================================
// Types
// ============================================================================

export type PlanType = "free" | "pro" | "enterprise";

// ============================================================================
// Wizard Schema
// ============================================================================

export const wizardSchema = {
  facts: {
    currentStep: t.number(),
    totalSteps: t.number(),
    advanceRequested: t.boolean(),
    email: t.string(),
    password: t.string(),
    name: t.string(),
    company: t.string(),
    plan: t.string<PlanType>(),
    newsletter: t.boolean(),
    submitted: t.boolean(),
  },
  derivations: {
    step0Valid: t.boolean(),
    step1Valid: t.boolean(),
    step2Valid: t.boolean(),
    currentStepValid: t.boolean(),
    canAdvance: t.boolean(),
    canGoBack: t.boolean(),
    progress: t.number(),
    isLastStep: t.boolean(),
  },
  events: {
    requestAdvance: {},
    goBack: {},
    setField: { field: t.string(), value: t.object<unknown>() },
    reset: {},
  },
  requirements: {
    ADVANCE_STEP: {},
    SUBMIT_FORM: {},
  },
} satisfies ModuleSchema;

// ============================================================================
// Wizard Module
// ============================================================================

export const wizardModule = createModule("wizard", {
  schema: wizardSchema,

  init: (facts) => {
    facts.currentStep = 0;
    facts.totalSteps = 3;
    facts.advanceRequested = false;
    facts.email = "";
    facts.password = "";
    facts.name = "";
    facts.company = "";
    facts.plan = "free";
    facts.newsletter = false;
    facts.submitted = false;
  },

  // ============================================================================
  // Derivations
  // ============================================================================

  derive: {
    step0Valid: (facts) => {
      return facts.email.includes("@") && facts.password.length >= 8;
    },

    step1Valid: (facts) => {
      return facts.name.trim().length > 0;
    },

    step2Valid: (facts) => {
      return facts.plan !== "";
    },

    currentStepValid: (facts, derived) => {
      if (facts.currentStep === 0) {
        return derived.step0Valid;
      }
      if (facts.currentStep === 1) {
        return derived.step1Valid;
      }
      if (facts.currentStep === 2) {
        return derived.step2Valid;
      }

      return false;
    },

    canAdvance: (facts, derived) => {
      return (
        derived.currentStepValid && facts.currentStep < facts.totalSteps - 1
      );
    },

    canGoBack: (facts) => {
      return facts.currentStep > 0;
    },

    progress: (facts) => {
      return Math.round(((facts.currentStep + 1) / facts.totalSteps) * 100);
    },

    isLastStep: (facts) => {
      return facts.currentStep === facts.totalSteps - 1;
    },
  },

  // ============================================================================
  // Events
  // ============================================================================

  events: {
    requestAdvance: (facts) => {
      facts.advanceRequested = true;
    },

    goBack: (facts) => {
      if (facts.currentStep > 0) {
        facts.currentStep = facts.currentStep - 1;
      }
    },

    setField: (facts, { field, value }) => {
      (facts as Record<string, unknown>)[field] = value;
    },

    reset: (facts) => {
      facts.currentStep = 0;
      facts.advanceRequested = false;
      facts.email = "";
      facts.password = "";
      facts.name = "";
      facts.company = "";
      facts.plan = "free";
      facts.newsletter = false;
      facts.submitted = false;
    },
  },

  // ============================================================================
  // Constraints
  // ============================================================================

  constraints: {
    // `when` receives `derived` as its second argument — the same derivations
    // the UI reads. No hand-rolled copy of the step-validity rules here.
    submit: {
      priority: 60,
      when: (facts, derived) => {
        const isLastStep = facts.currentStep === facts.totalSteps - 1;

        return (
          facts.advanceRequested && isLastStep && derived.currentStepValid
        );
      },
      require: { type: "SUBMIT_FORM" },
    },

    advance: {
      priority: 50,
      when: (facts, derived) => {
        const isLastStep = facts.currentStep === facts.totalSteps - 1;

        return (
          facts.advanceRequested && !isLastStep && derived.currentStepValid
        );
      },
      require: { type: "ADVANCE_STEP" },
    },
  },

  // ============================================================================
  // Resolvers
  // ============================================================================

  resolvers: {
    advanceStep: {
      requirement: "ADVANCE_STEP",
      resolve: async (_req, context) => {
        context.facts.currentStep = context.facts.currentStep + 1;
        context.facts.advanceRequested = false;
      },
    },

    submitForm: {
      requirement: "SUBMIT_FORM",
      timeout: 10000,
      resolve: async (_req, context) => {
        // Simulate API submission
        await new Promise((resolve) => setTimeout(resolve, 800));
        context.facts.submitted = true;
        context.facts.advanceRequested = false;
      },
    },
  },
});

// ============================================================================
// Validation Schema
// ============================================================================

export const validationSchema = {
  facts: {
    emailAvailable: t.boolean(),
    checkingEmail: t.boolean(),
    emailChecked: t.string(),
  },
  derivations: {},
  events: {},
  requirements: {
    CHECK_EMAIL: { email: t.string() },
  },
} satisfies ModuleSchema;

// ============================================================================
// Validation Module
// ============================================================================

export const validationModule = createModule("validation", {
  schema: validationSchema,

  crossModuleDeps: { wizard: wizardSchema },

  init: (facts) => {
    facts.emailAvailable = true;
    facts.checkingEmail = false;
    facts.emailChecked = "";
  },

  // ============================================================================
  // Constraints
  // ============================================================================

  constraints: {
    checkEmail: {
      when: (facts) => {
        const email = facts.wizard.email;
        const checked = facts.self.emailChecked;

        return email.includes("@") && email !== checked;
      },
      require: (facts) => ({
        type: "CHECK_EMAIL",
        email: facts.wizard.email,
      }),
    },
  },

  // ============================================================================
  // Resolvers
  // ============================================================================

  resolvers: {
    checkEmail: {
      requirement: "CHECK_EMAIL",
      resolve: async (req, context) => {
        context.facts.checkingEmail = true;

        try {
          // Simulate API availability check
          await new Promise((resolve) => setTimeout(resolve, 500));
          context.facts.emailAvailable = req.email !== "taken@test.com";
          context.facts.emailChecked = req.email;
        } finally {
          context.facts.checkingEmail = false;
        }
      },
    },
  },
});

// ============================================================================
// System
// ============================================================================

export const system = createSystem({
  modules: {
    wizard: wizardModule,
    validation: validationModule,
  },
  trace: true,
  plugins: [
    devtoolsPlugin({ name: "form-wizard" }),
    persistencePlugin({
      storage: localStorage,
      key: "form-wizard-draft",
      include: [
        "wizard::email",
        "wizard::name",
        "wizard::company",
        "wizard::plan",
        "wizard::currentStep",
      ],
    }),
  ],
});
```
