more plans
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 12m0s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 12m0s
This commit is contained in:
@@ -1,11 +1,19 @@
|
||||
;; Docs page content — fully self-contained, no Python intermediaries
|
||||
|
||||
(defcomp ~sx-home-content ()
|
||||
(div :id "main-content"
|
||||
(~sx-hero (highlight "(div :class \"p-4 bg-white rounded shadow\"\n (h1 :class \"text-2xl font-bold\" \"Hello\")\n (button :sx-get \"/api/data\"\n :sx-target \"#result\"\n \"Load data\"))" "lisp"))
|
||||
(~sx-philosophy)
|
||||
(~sx-how-it-works)
|
||||
(~sx-credits)))
|
||||
(div :id "main-content" :class "max-w-3xl mx-auto px-4 py-6"
|
||||
(highlight "(defcomp ~sx-header ()
|
||||
(a :href \"/\"
|
||||
:sx-get \"/\" :sx-target \"#main-panel\"
|
||||
:sx-select \"#main-panel\"
|
||||
:sx-swap \"outerHTML\" :sx-push-url \"true\"
|
||||
:class \"block max-w-3xl mx-auto px-4 pt-8 pb-4 text-center no-underline\"
|
||||
(span :class \"text-4xl font-bold font-mono text-violet-700 block mb-2\"
|
||||
\"(<sx>)\")
|
||||
(p :class \"text-lg text-stone-500 mb-1\"
|
||||
\"Framework free reactive hypermedia\")
|
||||
(p :class \"text-xs text-stone-400\"
|
||||
\"© Giles Bradshaw 2026\")))" "lisp")))
|
||||
|
||||
(defcomp ~docs-introduction-content ()
|
||||
(~doc-page :title "Introduction"
|
||||
|
||||
@@ -11,12 +11,11 @@
|
||||
|
||||
;; Logo + tagline + copyright — always shown at top of page area.
|
||||
(defcomp ~sx-header ()
|
||||
(div :class "max-w-3xl mx-auto px-4 pt-8 pb-4 text-center"
|
||||
(a :href "/"
|
||||
:sx-get "/" :sx-target "#main-panel" :sx-select "#main-panel"
|
||||
:sx-swap "outerHTML" :sx-push-url "true"
|
||||
:class "block mb-2"
|
||||
(span :class "text-4xl font-bold font-mono text-violet-700" "(<sx>)"))
|
||||
(a :href "/"
|
||||
:sx-get "/" :sx-target "#main-panel" :sx-select "#main-panel"
|
||||
:sx-swap "outerHTML" :sx-push-url "true"
|
||||
:class "block max-w-3xl mx-auto px-4 pt-8 pb-4 text-center no-underline"
|
||||
(span :class "text-4xl font-bold font-mono text-violet-700 block mb-2" "(<sx>)")
|
||||
(p :class "text-lg text-stone-500 mb-1"
|
||||
"Framework free reactive hypermedia")
|
||||
(p :class "text-xs text-stone-400"
|
||||
@@ -70,7 +69,7 @@
|
||||
;; Used by every defpage :content to embed nav inside the page content area.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~sx-doc (&key path &rest children)
|
||||
(defcomp ~sx-doc (&key path &rest children) :affinity :server
|
||||
(let ((nav-state (resolve-nav-path sx-nav-tree (or path "/"))))
|
||||
(<>
|
||||
(div :id "sx-nav" :class "mb-6"
|
||||
|
||||
@@ -188,7 +188,13 @@
|
||||
(dict :label "Live Streaming" :href "/plans/live-streaming"
|
||||
:summary "SSE and WebSocket transports for re-resolving suspense slots after initial page load — live data, real-time collaboration.")
|
||||
(dict :label "sx-web Platform" :href "/plans/sx-web-platform"
|
||||
:summary "sx-web.org as online development platform — embedded Claude Code, IPFS storage, sx-activity publishing, sx-ci testing. Author, stage, test, deploy from the browser.")))
|
||||
:summary "sx-web.org as online development platform — embedded Claude Code, IPFS storage, sx-activity publishing, sx-ci testing. Author, stage, test, deploy from the browser.")
|
||||
(dict :label "sx-forge" :href "/plans/sx-forge"
|
||||
:summary "Git forge in SX — repositories, issues, pull requests, CI, permissions, and federation. Configuration as macros, diffs as components.")
|
||||
(dict :label "sx-swarm" :href "/plans/sx-swarm"
|
||||
:summary "Container orchestration in SX — service definitions, environment macros, deploy pipelines. Replace YAML with a real language.")
|
||||
(dict :label "sx-proxy" :href "/plans/sx-proxy"
|
||||
:summary "Reverse proxy in SX — routes, TLS, middleware chains, load balancing. Macros generate config from the same service definitions as the orchestrator.")))
|
||||
|
||||
(define reactive-islands-nav-items (list
|
||||
(dict :label "Overview" :href "/reactive-islands/"
|
||||
|
||||
157
sx/sx/plans/sx-forge.sx
Normal file
157
sx/sx/plans/sx-forge.sx
Normal file
@@ -0,0 +1,157 @@
|
||||
;; sx-forge — SX-based Git Forge
|
||||
;; Plan: a Gitea/Forgejo-style git hosting platform where everything —
|
||||
;; repositories, issues, pull requests, CI, permissions — is SX.
|
||||
|
||||
(defcomp ~plan-sx-forge-content ()
|
||||
(~doc-page :title "sx-forge: Git Forge in SX"
|
||||
|
||||
(~doc-section :title "Vision" :id "vision"
|
||||
(p "A git forge where the entire interface, configuration, and automation layer "
|
||||
"is written in SX. Repositories are browsed, issues are filed, pull requests "
|
||||
"are reviewed, and CI pipelines are triggered — all through SX components "
|
||||
"rendered via the same hypermedia pipeline as every other SX app.")
|
||||
(p "Configuration is SX. Webhooks are SX. Access control policies are SX macros "
|
||||
"that expand to permission checks. Repository templates are defcomps. "
|
||||
"The forge doesn't use SX — it " (em "is") " SX."))
|
||||
|
||||
(~doc-section :title "Why" :id "why"
|
||||
(p "Gitea/Forgejo are excellent but they're Go binaries with YAML/INI config, "
|
||||
"Markdown rendering, and a template engine that's separate from the application logic. "
|
||||
"Every layer speaks a different language.")
|
||||
(p "sx-forge collapses these layers:")
|
||||
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
|
||||
(li "Repository browsing = SX components rendering git tree objects")
|
||||
(li "Issue tracking = SX forms with sx-post, stored as content-addressed SX documents")
|
||||
(li "Pull requests = SX diff viewer + sx-activity for review comments")
|
||||
(li "CI integration = sx-ci pipelines triggered by push hooks")
|
||||
(li "Configuration = SX s-expressions, manipulated by macros")
|
||||
(li "Access control = SX macros that expand to permission predicates")
|
||||
(li "API = SX wire format (text/sx) alongside JSON for compatibility")))
|
||||
|
||||
(~doc-section :title "Architecture" :id "architecture"
|
||||
(div :class "overflow-x-auto mt-4"
|
||||
(table :class "w-full text-sm text-left"
|
||||
(thead
|
||||
(tr :class "border-b border-stone-200"
|
||||
(th :class "py-2 px-3 font-semibold text-stone-700" "Layer")
|
||||
(th :class "py-2 px-3 font-semibold text-stone-700" "Implementation")
|
||||
(th :class "py-2 px-3 font-semibold text-stone-700" "Notes")))
|
||||
(tbody :class "text-stone-600"
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "py-2 px-3 font-semibold" "Git backend")
|
||||
(td :class "py-2 px-3" "libgit2 or shell-out to git")
|
||||
(td :class "py-2 px-3" "Smart HTTP + SSH protocols. Bare repos on disk."))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "py-2 px-3 font-semibold" "UI")
|
||||
(td :class "py-2 px-3" "SX components (defcomp)")
|
||||
(td :class "py-2 px-3" "Tree browser, diff viewer, blame, commit log — all defcomps."))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "py-2 px-3 font-semibold" "Issues / PRs")
|
||||
(td :class "py-2 px-3" "SX documents on IPFS")
|
||||
(td :class "py-2 px-3" "Content-addressed. Federated via sx-activity."))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "py-2 px-3 font-semibold" "CI")
|
||||
(td :class "py-2 px-3" "sx-ci pipelines")
|
||||
(td :class "py-2 px-3" "Push hook triggers pipeline. Results as SX components."))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "py-2 px-3 font-semibold" "Auth")
|
||||
(td :class "py-2 px-3" "OAuth2 + SX policy macros")
|
||||
(td :class "py-2 px-3" "Permissions are macro-expanded predicates."))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "py-2 px-3 font-semibold" "Config")
|
||||
(td :class "py-2 px-3" "SX s-expressions")
|
||||
(td :class "py-2 px-3" "forge.sx per-instance. repo.sx per-repo."))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "py-2 px-3 font-semibold" "Federation")
|
||||
(td :class "py-2 px-3" "sx-activity (ActivityPub)")
|
||||
(td :class "py-2 px-3" "Cross-instance PRs, issues, stars, forks."))))))
|
||||
|
||||
(~doc-section :title "Configuration as SX" :id "config"
|
||||
(p "Instance configuration is an SX file, not YAML or INI:")
|
||||
(highlight "(define forge-config
|
||||
{:name \"Rose Ash Forge\"
|
||||
:domain \"forge.rose-ash.com\"
|
||||
:ssh-port 2222
|
||||
:storage {:backend :filesystem :root \"/data/repos\"}
|
||||
:auth {:provider :oauth2
|
||||
:issuer \"https://account.rose-ash.com\"}
|
||||
:federation {:enabled true
|
||||
:allowlist (list \"*.rose-ash.com\")}
|
||||
:ci {:runner :sx-ci
|
||||
:default-pipeline \"ci/default.sx\"}})" "lisp")
|
||||
(p "Macros transform configuration:")
|
||||
(highlight ";; Macro: generate mirror config from upstream
|
||||
(defmacro mirror-repo (name upstream)
|
||||
`(define-repo ,name
|
||||
{:mirror true
|
||||
:upstream ,upstream
|
||||
:sync-interval 3600
|
||||
:ci false}))" "lisp")
|
||||
(p "Per-repository configuration lives in " (code "repo.sx") " at the repo root:")
|
||||
(highlight "(define repo-config
|
||||
{:default-branch \"main\"
|
||||
:ci (pipeline
|
||||
(stage :test (sx-ci/run \"test.sx\"))
|
||||
(stage :deploy
|
||||
(when-branch \"main\"
|
||||
(sx-ci/deploy :target :production))))
|
||||
:permissions
|
||||
{:push (or (role? :maintainer) (role? :admin))
|
||||
:merge-pr (and (ci-passed?) (approved-by? 1))
|
||||
:admin (role? :admin)}})" "lisp"))
|
||||
|
||||
(~doc-section :title "SX Diff Viewer" :id "diff-viewer"
|
||||
(p "Diffs rendered as SX components, not pre-formatted text:")
|
||||
(highlight ";; The diff viewer is a defcomp, composable like any other
|
||||
(defcomp ~diff-view (&key diff)
|
||||
(map (fn (hunk)
|
||||
(~diff-hunk
|
||||
:file (get hunk \"file\")
|
||||
:old-start (get hunk \"old-start\")
|
||||
:new-start (get hunk \"new-start\")
|
||||
:lines (get hunk \"lines\")))
|
||||
(get diff \"hunks\")))" "lisp")
|
||||
(p "Because diffs are SX data, macros can transform them:")
|
||||
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
|
||||
(li "Syntax highlighting via the same " (code "highlight") " helper used everywhere")
|
||||
(li "Inline review comments as SX forms (sx-post to comment endpoint)")
|
||||
(li "Suggestion blocks — click to apply a proposed change")
|
||||
(li "SX-aware diffs — show component-level changes, not just line changes")))
|
||||
|
||||
(~doc-section :title "Federated Forge" :id "federation"
|
||||
(p "sx-activity enables cross-instance collaboration:")
|
||||
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
|
||||
(li (strong "Cross-instance PRs") " — open a PR from your fork on another instance")
|
||||
(li (strong "Federated issues") " — file an issue on a remote repo from your instance")
|
||||
(li (strong "Stars and forks") " — ActivityPub Follow/Like activities")
|
||||
(li (strong "Mirror sync") " — subscribe to upstream changes via sx-activity")
|
||||
(li (strong "Review comments") " — threaded discussions federated as SX documents"))
|
||||
(p "Every issue, comment, and review is a content-addressed SX document on IPFS. "
|
||||
"Federation distributes references. The content is permanent and verifiable."))
|
||||
|
||||
(~doc-section :title "Git Operations as IO Primitives" :id "git-ops"
|
||||
(p "Git operations exposed as SX IO primitives in boundary.sx:")
|
||||
(highlight ";; boundary.sx additions
|
||||
(io git-log (repo &key branch limit offset) list)
|
||||
(io git-tree (repo ref path) list)
|
||||
(io git-blob (repo ref path) string)
|
||||
(io git-diff (repo base head) dict)
|
||||
(io git-refs (repo) list)
|
||||
(io git-commit (repo message files &key author) dict)
|
||||
(io git-create-branch (repo name from) dict)
|
||||
(io git-merge (repo source target &key strategy) dict)" "lisp")
|
||||
(p "Pages use these directly — no controller layer, no ORM:"))
|
||||
|
||||
(~doc-section :title "Implementation Path" :id "implementation"
|
||||
(ol :class "space-y-3 text-stone-600 list-decimal pl-5"
|
||||
(li (strong "Phase 1: Read-only browser") " — git-tree, git-blob, git-log, git-diff as IO primitives. "
|
||||
"SX components for tree view, blob view, commit log, diff view.")
|
||||
(li (strong "Phase 2: Issues") " — SX forms for create/edit. Content-addressed storage. "
|
||||
"Labels, milestones, assignees as SX data.")
|
||||
(li (strong "Phase 3: Pull requests") " — fork model, diff + review UI, merge strategies. "
|
||||
"CI status checks from sx-ci.")
|
||||
(li (strong "Phase 4: CI integration") " — push hooks trigger sx-ci pipelines. "
|
||||
"Results rendered as SX components on the PR page.")
|
||||
(li (strong "Phase 5: Federation") " — sx-activity for cross-instance PRs, issues, stars.")
|
||||
(li (strong "Phase 6: Admin") " — SX macro-based permission policies. "
|
||||
"Instance and org management via SX config.")))))
|
||||
186
sx/sx/plans/sx-proxy.sx
Normal file
186
sx/sx/plans/sx-proxy.sx
Normal file
@@ -0,0 +1,186 @@
|
||||
;; sx-proxy — SX-based Reverse Proxy
|
||||
;; Plan: a Caddy-style reverse proxy where routes, TLS, middleware,
|
||||
;; and load balancing are SX s-expressions manipulated by macros.
|
||||
|
||||
(defcomp ~plan-sx-proxy-content ()
|
||||
(~doc-page :title "sx-proxy: Reverse Proxy in SX"
|
||||
|
||||
(~doc-section :title "Vision" :id "vision"
|
||||
(p "A reverse proxy where routing rules, TLS configuration, middleware chains, "
|
||||
"and load balancing policies are SX. Not a config file that gets parsed — "
|
||||
"actual SX that gets evaluated. Macros generate routes from service definitions. "
|
||||
"The proxy config is derived from the same stack definition as the deployment.")
|
||||
(p "Caddy's Caddyfile is close — it's almost a language. But it stops short. "
|
||||
"No functions, no macros, no computed values, no integration with the rest of the stack. "
|
||||
"sx-proxy goes all the way: the proxy configuration " (em "is") " the application."))
|
||||
|
||||
(~doc-section :title "Why" :id "why"
|
||||
(p "Every proxy config language reinvents the same features badly:")
|
||||
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
|
||||
(li "Nginx: custom DSL with if-is-evil, no real conditionals, include for composition")
|
||||
(li "Caddy: Caddyfile is nice but no functions, no macros, no types")
|
||||
(li "Traefik: labels on Docker containers — stringly-typed, no validation")
|
||||
(li "HAProxy: line-oriented config, ACLs as string matching, no abstraction"))
|
||||
(p "All of them: separate config from application. "
|
||||
"SX unifies them. The proxy reads the same service definitions "
|
||||
"that the orchestrator deploys."))
|
||||
|
||||
(~doc-section :title "Route Definitions" :id "routes"
|
||||
(p "Routes as SX, with macros for common patterns:")
|
||||
(highlight ";; Basic route definition
|
||||
(route blog.rose-ash.com
|
||||
:upstream \"http://blog:8000\"
|
||||
:tls :auto)
|
||||
|
||||
;; Service route macro — generates from service definition
|
||||
(defmacro service-route (service)
|
||||
(let ((domain (service-domain service))
|
||||
(port (service-port service)))
|
||||
`(route ,domain
|
||||
:upstream ,(str \"http://\" (get service \"name\") \":\" port)
|
||||
:tls :auto
|
||||
:headers {:X-Forwarded-Proto \"https\"
|
||||
:X-Real-IP (client-ip)})))
|
||||
|
||||
;; Generate all routes from the stack definition
|
||||
(define routes
|
||||
(map service-route (get rose-ash-stack \"services\")))" "lisp")
|
||||
(p "One macro generates all routes from the same service definitions "
|
||||
"that sx-swarm uses for deployment. Change the service, the route updates."))
|
||||
|
||||
(~doc-section :title "Middleware as Composition" :id "middleware"
|
||||
(p "Middleware chains are function composition:")
|
||||
(highlight ";; Middleware are functions: request -> response -> response
|
||||
(define rate-limit
|
||||
(middleware :name :rate-limit
|
||||
:window \"1m\" :max 100 :key (client-ip)))
|
||||
|
||||
(define cors
|
||||
(middleware :name :cors
|
||||
:origins (list \"*.rose-ash.com\")
|
||||
:methods (list :GET :POST :PUT :DELETE)
|
||||
:headers (list :Authorization :Content-Type)))
|
||||
|
||||
(define auth-required
|
||||
(middleware :name :auth
|
||||
:provider :oauth2
|
||||
:redirect \"https://account.rose-ash.com/login\"))
|
||||
|
||||
;; Compose middleware chains
|
||||
(define public-chain
|
||||
(chain rate-limit cors))
|
||||
|
||||
(define private-chain
|
||||
(chain rate-limit cors auth-required))
|
||||
|
||||
;; Apply to routes
|
||||
(route account.rose-ash.com
|
||||
:upstream \"http://account:8000\"
|
||||
:middleware private-chain
|
||||
:tls :auto)" "lisp")
|
||||
(p "Middleware is data. Chains compose with " (code "chain") ". "
|
||||
"Apply different chains to different routes. "
|
||||
"No nginx location blocks, no Caddy handle nesting."))
|
||||
|
||||
(~doc-section :title "TLS Configuration" :id "tls"
|
||||
(p "TLS as SX with macro-generated cert management:")
|
||||
(highlight ";; Auto TLS via ACME (like Caddy)
|
||||
(define tls-auto
|
||||
{:provider :acme
|
||||
:ca \"https://acme-v02.api.letsencrypt.org/directory\"
|
||||
:email \"admin@rose-ash.com\"
|
||||
:storage \"/data/certs\"})
|
||||
|
||||
;; Wildcard via DNS challenge
|
||||
(define tls-wildcard
|
||||
{:provider :acme
|
||||
:ca \"https://acme-v02.api.letsencrypt.org/directory\"
|
||||
:dns-challenge {:provider :cloudflare
|
||||
:api-token (from-secret \"cf-token\")}
|
||||
:domains (list \"*.rose-ash.com\")})
|
||||
|
||||
;; Per-route TLS macro
|
||||
(defmacro with-tls (config &rest routes)
|
||||
`(map (fn (r) (assoc r :tls ,config)) (list ,@routes)))" "lisp"))
|
||||
|
||||
(~doc-section :title "Load Balancing" :id "load-balancing"
|
||||
(p "Load balancing policies as SX values:")
|
||||
(highlight ";; Load balancing strategies
|
||||
(define round-robin (lb :strategy :round-robin))
|
||||
(define least-conn (lb :strategy :least-connections))
|
||||
(define ip-hash (lb :strategy :ip-hash))
|
||||
|
||||
;; Health-checked upstream pool
|
||||
(define blog-pool
|
||||
(upstream-pool
|
||||
:backends (list \"blog-1:8000\" \"blog-2:8000\" \"blog-3:8000\")
|
||||
:strategy least-conn
|
||||
:health-check {:path \"/health\"
|
||||
:interval \"10s\"
|
||||
:timeout \"3s\"
|
||||
:unhealthy-threshold 3}))
|
||||
|
||||
(route blog.rose-ash.com
|
||||
:upstream blog-pool
|
||||
:tls :auto)" "lisp"))
|
||||
|
||||
(~doc-section :title "Dynamic Reconfiguration" :id "dynamic"
|
||||
(p "The proxy evaluates SX — so config can be dynamic:")
|
||||
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
|
||||
(li (strong "Hot reload") " — change the SX, proxy re-evaluates. No restart.")
|
||||
(li (strong "API-driven") " — sx-post new routes. The proxy is an SX app with endpoints.")
|
||||
(li (strong "Swarm-aware") " — subscribe to swarm-status changes, "
|
||||
"auto-update upstreams when services scale or move.")
|
||||
(li (strong "Feature flags") " — route traffic based on SX predicates "
|
||||
"(header match, percentage split, user attribute).")
|
||||
(li (strong "A/B testing") " — split traffic via SX expressions, "
|
||||
"not nginx map blocks or Traefik weighted services."))
|
||||
(highlight ";; Traffic splitting — 90% to v2, 10% to v1
|
||||
(route blog.rose-ash.com
|
||||
:upstream (traffic-split
|
||||
{:weight 90 :backend \"blog-v2:8000\"}
|
||||
{:weight 10 :backend \"blog-v1:8000\"})
|
||||
:tls :auto)
|
||||
|
||||
;; Feature flag routing
|
||||
(route blog.rose-ash.com
|
||||
:upstream (if (header? \"X-Beta\" \"true\")
|
||||
\"blog-beta:8000\"
|
||||
\"blog-prod:8000\")
|
||||
:tls :auto)" "lisp"))
|
||||
|
||||
(~doc-section :title "Integration with sx-swarm" :id "integration"
|
||||
(p "sx-proxy and sx-swarm share the same service definitions:")
|
||||
(highlight ";; One source of truth
|
||||
(defservice blog
|
||||
:image \"rose-ash/blog:latest\"
|
||||
:port 8000
|
||||
:domain \"blog.rose-ash.com\"
|
||||
:replicas 2)
|
||||
|
||||
;; sx-swarm reads: image, port, replicas -> container orchestration
|
||||
;; sx-proxy reads: port, domain -> route generation
|
||||
;; sx-ci reads: image -> build pipeline
|
||||
;; sx-forge reads: domain -> webhook URLs
|
||||
|
||||
;; Everything derived from the same definition.
|
||||
;; Change it once, everything updates." "lisp")
|
||||
(p "This is the point of using one language for everything. "
|
||||
"The proxy, the orchestrator, the CI, and the forge all consume "
|
||||
"the same SX service definitions. No YAML-to-Caddyfile-to-Dockerfile translation."))
|
||||
|
||||
(~doc-section :title "Implementation Path" :id "implementation"
|
||||
(ol :class "space-y-3 text-stone-600 list-decimal pl-5"
|
||||
(li (strong "Phase 1: Config compiler") " — SX route definitions compiled to Caddy JSON API. "
|
||||
"Use Caddy as the runtime, SX as the config language.")
|
||||
(li (strong "Phase 2: Middleware macros") " — defmacro for common middleware patterns. "
|
||||
"Chain composition. Apply to route groups.")
|
||||
(li (strong "Phase 3: Swarm integration") " — auto-generate routes from sx-swarm service defs. "
|
||||
"Hot reload on swarm state changes.")
|
||||
(li (strong "Phase 4: Dynamic routing") " — traffic splitting, feature flags, A/B testing. "
|
||||
"SX predicates evaluated per-request.")
|
||||
(li (strong "Phase 5: Native proxy") " — replace Caddy with SX-native HTTP proxy. "
|
||||
"Event loop in the host language, routing logic in SX. "
|
||||
"Like phase 5 of sx-swarm: the ambitious endgame."))
|
||||
(p "Phase 1-3 are practical today — Caddy's JSON API is a clean compilation target. "
|
||||
"The SX layer adds macros, composition, and integration that Caddyfile lacks."))))
|
||||
171
sx/sx/plans/sx-swarm.sx
Normal file
171
sx/sx/plans/sx-swarm.sx
Normal file
@@ -0,0 +1,171 @@
|
||||
;; sx-swarm — SX-based Container Orchestration
|
||||
;; Plan: Docker Swarm management where stack definitions, service configs,
|
||||
;; and deployment logic are SX s-expressions manipulated by macros.
|
||||
|
||||
(defcomp ~plan-sx-swarm-content ()
|
||||
(~doc-page :title "sx-swarm: Container Orchestration in SX"
|
||||
|
||||
(~doc-section :title "Vision" :id "vision"
|
||||
(p "Replace docker-compose.yml and Docker Swarm stack files with SX. "
|
||||
"Service definitions are defcomps. Environment configs are macros that expand "
|
||||
"differently per target (dev, staging, production). Deployments are SX pipelines "
|
||||
"executed by sx-ci. The entire infrastructure is the same language as the application.")
|
||||
(p "YAML is a data format pretending to be a configuration language. "
|
||||
"It has no functions, no macros, no composition, no type checking. "
|
||||
"SX has all of these. A service definition is a value. A deployment is a function. "
|
||||
"An environment override is a macro."))
|
||||
|
||||
(~doc-section :title "Why Not YAML" :id "why-not-yaml"
|
||||
(p "Docker Compose and Swarm stack files share a fundamental problem: "
|
||||
"they're static data with ad-hoc templating bolted on. "
|
||||
"Variable substitution (${VAR}), extension fields (x-), YAML anchors (&/*) — "
|
||||
"all workarounds for not having a real language.")
|
||||
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
|
||||
(li "No functions — can't abstract repeated patterns")
|
||||
(li "No macros — can't generate config at definition time")
|
||||
(li "No conditionals — can't branch on environment")
|
||||
(li "No types — a typo in an environment variable is a runtime error")
|
||||
(li "No composition — merging files is fragile and order-dependent")
|
||||
(li "No verification — can't check constraints before deploy"))
|
||||
(p "SX solves all of these because it's a programming language, not a data format."))
|
||||
|
||||
(~doc-section :title "Service Definitions" :id "services"
|
||||
(p "Services defined as SX, with macros for common patterns:")
|
||||
(highlight ";; Base service macro — shared defaults
|
||||
(defmacro defservice (name &rest body)
|
||||
`(define ,name
|
||||
(merge-service
|
||||
{:restart-policy {:condition :on-failure :max-attempts 3}
|
||||
:logging {:driver :json-file :options {:max-size \"10m\"}}
|
||||
:networks (list \"internal\")}
|
||||
(dict ,@body))))
|
||||
|
||||
;; A concrete service
|
||||
(defservice blog-service
|
||||
:image \"rose-ash/blog:latest\"
|
||||
:ports (list \"8001:8000\")
|
||||
:environment (env-for :blog)
|
||||
:volumes (list \"./blog:/app/blog:ro\")
|
||||
:depends-on (list :postgres :redis)
|
||||
:deploy {:replicas 2
|
||||
:update-config {:parallelism 1 :delay \"10s\"}})" "lisp")
|
||||
(p "The " (code "defservice") " macro injects shared defaults. "
|
||||
"Every service gets restart policy, logging, and network config for free. "
|
||||
"Override any field by specifying it in the body."))
|
||||
|
||||
(~doc-section :title "Environment Macros" :id "environments"
|
||||
(p "Environment-specific config via macros, not file merging:")
|
||||
(highlight ";; Environment macro — expands differently per target
|
||||
(defmacro env-for (service)
|
||||
(let ((base (service-env-base service))
|
||||
(target (current-deploy-target)))
|
||||
(case target
|
||||
:dev (merge base
|
||||
{:DEBUG \"true\"
|
||||
:DATABASE_URL (str \"postgresql://\" service \"_dev/\" service)})
|
||||
:staging (merge base
|
||||
{:DATABASE_URL (from-secret (str service \"-db-staging\"))})
|
||||
:production (merge base
|
||||
{:DATABASE_URL (from-secret (str service \"-db-prod\"))
|
||||
:SENTRY_DSN (from-secret \"sentry-dsn\")}))))
|
||||
|
||||
;; Volume mounts differ per environment
|
||||
(defmacro dev-volumes (service)
|
||||
`(list ,(str \"./\" service \":/app/\" service)
|
||||
,(str \"./shared:/app/shared\")
|
||||
\"./dev.sh:/app/dev.sh:ro\"))
|
||||
|
||||
;; Production: no bind mounts, just named volumes
|
||||
(defmacro prod-volumes (service)
|
||||
`(list ,(str service \"-data:/data\")))" "lisp")
|
||||
(p "No more docker-compose.yml + docker-compose.dev.yml + docker-compose.prod.yml. "
|
||||
"One definition, macros handle the rest."))
|
||||
|
||||
(~doc-section :title "Stack Composition" :id "composition"
|
||||
(p "Stacks compose like functions:")
|
||||
(highlight ";; Infrastructure services shared across all stacks
|
||||
(define infra-services
|
||||
(list
|
||||
(defservice postgres
|
||||
:image \"postgres:16\"
|
||||
:volumes (list \"pg-data:/var/lib/postgresql/data\")
|
||||
:environment {:POSTGRES_PASSWORD (from-secret \"pg-pass\")})
|
||||
(defservice redis
|
||||
:image \"redis:7-alpine\"
|
||||
:volumes (list \"redis-data:/data\"))
|
||||
(defservice pgbouncer
|
||||
:image \"edoburu/pgbouncer\"
|
||||
:depends-on (list :postgres))))
|
||||
|
||||
;; Full stack = infra + app services
|
||||
(define rose-ash-stack
|
||||
(stack :name \"rose-ash\"
|
||||
:services (concat infra-services app-services)
|
||||
:networks (list
|
||||
(network :name \"internal\" :driver :overlay :internal true)
|
||||
(network :name \"public\" :driver :overlay))
|
||||
:volumes (list
|
||||
(volume :name \"pg-data\" :driver :local)
|
||||
(volume :name \"redis-data\" :driver :local))))" "lisp"))
|
||||
|
||||
(~doc-section :title "Deploy as SX Pipeline" :id "deploy"
|
||||
(p "Deployment is an sx-ci pipeline, not a shell script:")
|
||||
(highlight "(define deploy-pipeline
|
||||
(pipeline :name \"deploy\"
|
||||
(stage :build
|
||||
(map (fn (svc)
|
||||
(docker-build
|
||||
:context (str \"./\" (get svc \"name\"))
|
||||
:tag (str \"rose-ash/\" (get svc \"name\") \":\" (git-sha))))
|
||||
(changed-services)))
|
||||
(stage :push
|
||||
(map (fn (svc)
|
||||
(docker-push (service-image svc)))
|
||||
(changed-services)))
|
||||
(stage :deploy
|
||||
(swarm-deploy rose-ash-stack
|
||||
:target (current-deploy-target)
|
||||
:rolling true
|
||||
:health-check-interval \"5s\"))))" "lisp"))
|
||||
|
||||
(~doc-section :title "Swarm Operations as IO" :id "swarm-ops"
|
||||
(p "Swarm management exposed as SX IO primitives:")
|
||||
(highlight ";; boundary.sx additions
|
||||
(io swarm-deploy (stack &key target rolling) dict)
|
||||
(io swarm-status (&key stack service) list)
|
||||
(io swarm-scale (service replicas) dict)
|
||||
(io swarm-rollback (service) dict)
|
||||
(io swarm-logs (service &key since tail follow) list)
|
||||
(io swarm-inspect (service) dict)
|
||||
(io swarm-nodes () list)
|
||||
(io swarm-secrets (&key create delete) dict)
|
||||
(io docker-build (&key context tag dockerfile) dict)
|
||||
(io docker-push (image) dict)" "lisp")
|
||||
(p "These compose with sx-ci and the forge — push to forge triggers CI, "
|
||||
"CI runs tests, tests pass, deploy pipeline executes, swarm updates."))
|
||||
|
||||
(~doc-section :title "Health and Monitoring" :id "monitoring"
|
||||
(p "Service health as live SX components:")
|
||||
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
|
||||
(li (strong "Dashboard") " — defcomp rendering swarm-status, auto-refreshing via sx-get polling")
|
||||
(li (strong "Log viewer") " — swarm-logs streamed via SSE, rendered as SX")
|
||||
(li (strong "Alerts") " — SX predicates on service state, notifications via sx-activity")
|
||||
(li (strong "Rollback") " — one-click rollback via swarm-rollback IO primitive"))
|
||||
(p "The monitoring dashboard is an SX page like any other. "
|
||||
"No Grafana, no separate monitoring stack. Same language, same renderer, same platform."))
|
||||
|
||||
(~doc-section :title "Implementation Path" :id "implementation"
|
||||
(ol :class "space-y-3 text-stone-600 list-decimal pl-5"
|
||||
(li (strong "Phase 1: Stack definition") " — SX data structures for services, networks, volumes. "
|
||||
"Compiler to docker-compose.yml / docker stack deploy format.")
|
||||
(li (strong "Phase 2: Environment macros") " — defmacro for env-for, dev-volumes, prod-volumes. "
|
||||
"Single source file, multiple targets.")
|
||||
(li (strong "Phase 3: Deploy pipeline") " — sx-ci integration. Build, push, deploy as SX stages.")
|
||||
(li (strong "Phase 4: Swarm IO") " — boundary primitives wrapping Docker API. "
|
||||
"Dashboard and log viewer components.")
|
||||
(li (strong "Phase 5: Native orchestration") " — replace Docker Swarm entirely. "
|
||||
"SX-native container scheduling, networking, service discovery. "
|
||||
"The ultimate goal: no Docker, no Swarm — just SX managing containers directly."))
|
||||
(p "Phase 1-3 are pragmatic — compile SX to existing Docker tooling. "
|
||||
"Phase 5 is ambitious — replace Docker tooling with SX. "
|
||||
"The architecture supports both because the abstraction boundary is clean."))))
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
(defcomp ~doc-page (&key title &rest children)
|
||||
(div :class "max-w-4xl mx-auto px-6 py-8"
|
||||
(h1 :class "text-4xl font-bold text-stone-900 mb-8" title)
|
||||
(h1 :class "text-4xl font-bold text-stone-900 mb-8 text-center" title)
|
||||
(div :class "prose prose-stone max-w-none space-y-6" children)))
|
||||
|
||||
(defcomp ~doc-section (&key title id &rest children)
|
||||
|
||||
@@ -513,6 +513,9 @@
|
||||
"sx-ci" (~plan-sx-ci-content)
|
||||
"live-streaming" (~plan-live-streaming-content)
|
||||
"sx-web-platform" (~plan-sx-web-platform-content)
|
||||
"sx-forge" (~plan-sx-forge-content)
|
||||
"sx-swarm" (~plan-sx-swarm-content)
|
||||
"sx-proxy" (~plan-sx-proxy-content)
|
||||
:else (~plans-index-content))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user