https://github.com/brianium/sandestin
An effect system facilitating discoverable effects for humans and LLMs
https://github.com/brianium/sandestin
Last synced: 4 months ago
JSON representation
An effect system facilitating discoverable effects for humans and LLMs
- Host: GitHub
- URL: https://github.com/brianium/sandestin
- Owner: brianium
- Created: 2026-01-11T22:57:15.000Z (5 months ago)
- Default Branch: main
- Last Pushed: 2026-01-12T02:52:31.000Z (5 months ago)
- Last Synced: 2026-01-12T04:51:36.173Z (5 months ago)
- Language: Clojure
- Size: 55.7 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# ascolais/sandestin
An effect dispatch library for Clojure with schema-driven discoverability.
Sandestin provides a structured way to dispatch side effects while maintaining excellent introspection capabilities. It's designed to work seamlessly with LLM-assisted workflows and REPL-driven development.
## Features
- **Effect dispatch** - Dispatch vector-based effects with composable registries
- **Actions** - Pure functions that expand into effect sequences
- **Placeholders** - Late-bound value resolution from dispatch context
- **Interceptors** - Lifecycle hooks for instrumentation and control flow
- **Schema-driven** - Malli schemas for all registered items
- **Discoverability** - Built-in functions to describe, sample, search, and inspect
## Installation
Add to your `deps.edn`:
```clojure
{:deps
{io.github.brianium/sandestin {:git/tag "v0.5.0" :git/sha "526d4c5"}}}
```
## Quick Start
```clojure
(ns myapp.core
(:require [ascolais.sandestin :as s]))
;; Define a registry with effects
(def my-registry
{::s/effects
{:myapp/log
{::s/description "Log a message"
::s/schema [:tuple [:= :myapp/log] :string]
::s/handler (fn [_ctx _system msg]
(println msg)
:logged)}}})
;; Create a dispatch function
(def dispatch (s/create-dispatch [my-registry]))
;; Dispatch effects
(dispatch {} {} [[:myapp/log "Hello, Sandestin!"]])
;; => {:results [{:effect [:myapp/log "Hello, Sandestin!"], :res :logged}]
;; :errors []}
```
## Core Concepts
### Effects
Effects are side-effecting operations. Each effect has a handler that receives:
- `ctx` - Context map with `:dispatch`, `:dispatch-data`, `:system`
- `system` - The live system map (database connections, config, etc.)
- `& args` - Additional arguments from the effect vector
```clojure
{::s/effects
{:db/execute
{::s/description "Execute a SQL query"
::s/schema [:tuple [:= :db/execute] :string [:* :any]]
::s/system-keys [:datasource]
::s/handler (fn [{:keys [dispatch]} system sql & params]
(let [result (jdbc/execute! (:datasource system)
(into [sql] params))]
;; Optionally dispatch continuation effects
(dispatch {:result result} [[::log "Query complete"]])
result))}}}
```
The `dispatch` function in handler context supports three arities:
- `(dispatch fx)` — dispatch with current system and dispatch-data
- `(dispatch extra-dispatch-data fx)` — merge additional dispatch-data
- `(dispatch system-override extra-dispatch-data fx)` — merge into both system and dispatch-data
The 3-arity form enables effects to dispatch nested effects with a modified system context:
```clojure
;; Route nested effects to a different connection
(fn [{:keys [dispatch]} system connection-key child-fx]
(when-some [alt-conn (get-connection (:pool system) connection-key)]
(dispatch {:sse alt-conn} ; merged into system
{} ; extra dispatch-data
child-fx)))
```
### Actions
Actions are pure functions that transform state into effect vectors. They receive immutable state (extracted via `::system->state`) and return effects.
```clojure
{::s/actions
{:myapp/greet-user
{::s/description "Greet a user and log the event"
::s/schema [:tuple [:= :myapp/greet-user] :string]
::s/handler (fn [state username]
[[:myapp/log (str "Hello, " username "!")]
[:myapp/save-greeting {:user username :at (System/currentTimeMillis)}]])}}
::s/system->state
(fn [system] (:app-state system))}
```
### Placeholders
Placeholders resolve values from dispatch-data at dispatch time. They enable late binding of values, particularly useful for async continuations.
```clojure
{::s/placeholders
{:myapp/current-user
{::s/description "Get current user from dispatch context"
::s/schema :map
::s/handler (fn [dispatch-data]
(:current-user dispatch-data))}}
::s/effects
{:myapp/greet
{::s/handler (fn [_ctx _sys user]
(str "Hello, " (:name user) "!"))}}}
;; Usage with placeholder
(dispatch {} {:current-user {:name "Alice"}}
[[:myapp/greet [:myapp/current-user]]])
```
### Interceptors
Interceptors provide lifecycle hooks around dispatch, action expansion, and effect execution.
```clojure
(def logging-interceptor
{:id ::logging
:before-dispatch (fn [ctx] (tap> {:event :dispatch-start}) ctx)
:after-dispatch (fn [ctx] (tap> {:event :dispatch-end :errors (:errors ctx)}) ctx)
:before-effect (fn [ctx] (tap> {:event :effect :effect (:effect ctx)}) ctx)})
{::s/interceptors [logging-interceptor]}
```
Built-in interceptors:
- `ascolais.sandestin.interceptors/fail-fast` - Stop on first error
## Discoverability
Sandestin is designed for LLM-assisted development. Use these functions to explore registered items:
### describe
```clojure
;; All items
(s/describe dispatch)
;; By type
(s/describe dispatch :effects)
(s/describe dispatch :actions)
(s/describe dispatch :placeholders)
;; Specific item
(s/describe dispatch :db/execute)
;; => {:ascolais.sandestin/key :db/execute
;; :ascolais.sandestin/type :effect
;; :ascolais.sandestin/description "Execute a SQL query"
;; :ascolais.sandestin/schema [:tuple ...]
;; :ascolais.sandestin/system-keys [:datasource]}
```
### sample
Generate sample data using Malli generators:
```clojure
(s/sample dispatch :db/execute)
;; => [:db/execute "generated-string" 42]
(s/sample dispatch :db/execute 3)
;; => ([:db/execute ...] [:db/execute ...] [:db/execute ...])
```
### grep
Search by pattern:
```clojure
(s/grep dispatch "database")
;; => ({:ascolais.sandestin/key :db/execute ...})
(s/grep dispatch #"log|save")
;; => items matching the regex
```
### schemas
Get all schemas as a map:
```clojure
(s/schemas dispatch)
;; => {:db/execute [:tuple ...], :myapp/log [:tuple ...], ...}
```
### system-schema
Get merged system requirements:
```clojure
(s/system-schema dispatch)
;; => {:datasource [...schema...], :config [...schema...]}
```
## Registry Structure
A registry is a map with these keys (all namespaced under `ascolais.sandestin`):
```clojure
{::s/effects {qualified-keyword -> EffectRegistration}
::s/actions {qualified-keyword -> ActionRegistration}
::s/placeholders {qualified-keyword -> PlaceholderRegistration}
::s/interceptors [Interceptor ...]
::s/system-schema {keyword -> MalliSchema}
::s/system->state (fn [system] state)}
```
### Registration Maps
```clojure
;; Effect
{::s/description "Human-readable description"
::s/schema [:tuple [:= :effect/key] ...args...]
::s/handler (fn [ctx system & args] result)
::s/system-keys [:datasource :config]} ; optional
;; Action
{::s/description "..."
::s/schema [:tuple [:= :action/key] ...args...]
::s/handler (fn [state & args] [[effects...]])}
;; Placeholder
{::s/description "..."
::s/schema MalliSchema ; schema for the resolved value
::s/handler (fn [dispatch-data & args] value)}
;; Interceptor
{:id :qualified/keyword
:before-dispatch (fn [ctx] ctx)
:after-dispatch (fn [ctx] ctx)
:before-action (fn [ctx] ctx)
:after-action (fn [ctx] ctx)
:before-effect (fn [ctx] ctx)
:after-effect (fn [ctx] ctx)}
```
## Composing Registries
Registries can be composed from multiple sources:
```clojure
(def dispatch
(s/create-dispatch
[[db/registry {:dbtype "postgresql"}] ; vector [fn & args]
auth/registry ; zero-arity fn
{:myapp/effects {...}}])) ; plain map
```
Merge rules:
- Effects, actions, placeholders: later wins on conflict (with tap> warning)
- Interceptors: concatenated in order
- system-schema: merged (later wins per key)
- system->state: last wins
## Dispatch Flow
1. Run before-dispatch interceptors
2. Interpolate placeholders in input
3. Expand actions to effects (with before/after-action interceptors)
4. Execute effects (with before/after-effect interceptors)
5. Run after-dispatch interceptors
6. Return `{:results [...] :errors [...]}`
## Development
### Start the REPL
```bash
clj -M:dev
```
### Development Workflow
```clojure
(dev) ; Switch to dev namespace
(start) ; Start system (opens Portal)
(reload) ; Reload changed namespaces
(restart) ; Full restart
```
### Testing
```bash
clj -X:test
```
## Claude Code Skills
Sandestin ships with Claude Code skills for LLM-assisted development:
- **`/fx-explore`** - Discover available effects, actions, and placeholders via REPL
- **`/fx-registry`** - Create new registries following project conventions
Install by copying to your skills directory:
```bash
cp -r .claude/skills/fx-explore ~/.claude/skills/
cp -r .claude/skills/fx-registry ~/.claude/skills/
```
## License
Copyright 2025 Brian Scaturro
Distributed under the Eclipse Public License version 1.0.