A monorepo for pull-based pattern matching and data transformation tools in Clojure/ClojureScript.
A local marketplace is available at claude-ctx/ with enforced workflows:
/plugin marketplace add ./claude-ctxThen enable in .claude/settings.local.json:
{
"enabledPlugins": {
"lasagna-clj@lasagna": true,
"lasagna-jj@lasagna": true
}
}| Plugin | What it does |
|---|---|
lasagna-clj |
Enforces /clojure skill before editing .clj files, paren repair |
lasagna-jj |
Blocks git commands (jj only), test reminders before commits |
All builds run from root via Babashka:
bb list # List all components
bb test # Test all components
bb test pattern # Test specific component
bb dev pattern # Start nREPL for component
bb clean # Clean all
bb clean pattern # Clean specific componentComponents are auto-discovered (any directory with deps.edn).
The same pattern syntax describes both reads and writes. Each component handles a distinct part of the round-trip:
pattern — compiles patterns into matcher functions, matches against ILookup data (READ path)
collection — wraps data sources in ILookup + Mutable, returns full entities from mutations
remote — receives patterns over HTTP, detects mutations vs reads, routes accordingly
Reads flow through pattern: remote compiles the pattern via match-fn, matches it against collections (which implement ILookup), returns bindings.
Writes flow through collection: remote detects the mutation via parse-mutation (variables in value = read, literals = write), calls coll/mutate! directly, returns the full entity. pattern is not involved in mutations.
A mutation pattern is still a pull. When a client sends {:posts {nil {:title "New"}}}, the response contains the full created entity: {posts {:post/id 42 :title "New" :created-at ...}}. The client should use this response to update local state — not discard it and re-fetch.
| Directory | Description | Status |
|---|---|---|
pattern/ |
Core pattern DSL for matching/transforming Clojure data | Active |
collection/ |
CRUD collection abstraction — wraps data sources in ILookup + Seqable + Counted + Mutable + Wireable | Active |
remote/ |
HTTP transport — sends patterns over the wire, detects errors in reads (partial success) and mutations | Active |
examples/flybot-site/ |
Flybot.sg site - public blog, employee authoring | Active |
examples/pull-playground/ |
Interactive SPA for learning pull patterns (sandbox + remote) | Active |
-
Create directory with
deps.edn:{:paths ["src"] :deps {org.clojure/clojure {:mvn/version "1.12.4"}} :aliases {:dev {:extra-paths ["notebook"] :extra-deps {io.github.robertluo/rich-comment-tests {:mvn/version "1.1.78"}}} :rct {:exec-fn com.mjdowney.rich-comment-tests.test-runner/run-tests-in-file-tree! :exec-args {:dirs #{"src"}}}}} -
For local dependencies on other components:
{:deps {local/pattern {:local/root "../pattern"}}} -
Component is auto-discovered by
bb list.
Core pattern DSL enabling declarative matching and transformation of Clojure data structures.
Source: pattern/src/sg/flybot/pullable/impl.cljc
Public API:
match-fn- Create pattern-matching functions with variable bindings (supports:schema,:rulesoptions)rule- Pattern → template transformation rulesapply-rules- Recursive tree transformation
Pattern syntax:
?x ; Bind value to x
?_ ; Wildcard (match anything)
?x? ; Optional (0-1)
?x* ; Zero or more
?x+ ; One or more
{} ; Map pattern
[] ; Sequence pattern
(?x :when pred) ; Constrained match
(?x :default val) ; Default on failureMap matching:
- Supports maps and any
ILookupimplementation (lazy data sources) - Maps preserve unmatched keys (passthrough semantics)
- ILookup returns only matched keys (can't enumerate all keys)
- Non-keyword keys for indexed lookup:
{{:id 1} ?result} - With Malli schemas: indexed lookup requires
:ilookup trueon collection (e.g.,[:vector {:ilookup true} ...])
Utilities: sg.flybot.pullable.util — variable? (check if symbol is ?-prefixed), contains-variables? (recursive tree-walk for nested patterns), vars-> (macro for destructuring vars maps).
Deep dive: See pattern/CLAUDE.md for full syntax catalog, architecture, and extension points.
Public company blog with employee-authored content. Demonstrates role-based API with pattern CRUD.
Access model (role-as-top-level):
| Role | Who | Permissions |
|---|---|---|
:guest |
Anonymous | Read posts |
:member |
Logged-in | Read + CRUD own posts + view history |
:admin |
Granted | CRUD any post |
:owner |
Config | All above + manage users/roles |
Stack: Clojure + ClojureScript, Datahike, http-kit, Replicant SPA, Google OAuth
Source: examples/flybot-site/src/sg/flybot/flybot_site/server/
Run:
bb dev examples/flybot-site # Start nREPL
# Then in REPL: (user/start!)API design (role-as-top-level):
;; Each role key contains its accessible resources
'{:guest {:posts ?all}} ; guest list
'{:guest {:posts {{:post/id 1} ?post}}} ; guest read
{:member {:posts {nil {:post/title "New"}}}} ; member create
{:member {:posts {{:post/id 1} {:post/title "X"}}}} ; member update own
{:member {:posts {{:post/id 1} nil}}} ; member delete own
'{:member {:posts/history {{:post/id 1} ?v}}} ; member history
{:admin {:posts {{:post/id 1} {:post/title "X"}}}} ; admin update any
'{:owner {:users ?all}} ; owner list usersKey patterns demonstrated:
- Role-as-top-level authorization (error map if session lacks role)
- ILookup-based collections for lazy data access
- Ownership enforcement via
coll/wrap-mutable - Non-enumerable resources via
coll/lookupwith delay-based laziness
Deploy any component by tagging from the repo root:
bb tag <component> # e.g. bb tag examples/pull-playgroundThis reads resources/version.edn, creates a git tag (<comp-name>-v<version>), and pushes it to origin. CI picks up the tag and deploys automatically.
| Component | Tag pattern | CI workflow | Target |
|---|---|---|---|
examples/pull-playground |
pull-playground-v* |
S3 + CloudFront | https://pattern.flybot.sg |
examples/flybot-site |
flybot-site-v* |
ECR container + App Runner | https://www.flybot.sg |
Bump resources/version.edn before tagging.
Uses Rich Comment Tests (RCT). Run bb test to execute all tests.
^:rct/test
(comment
(some-fn 1 2) ;=> expected-result
)