Files
rose-ash/sx/sx/etc/plan/art-dag-sx/index.sx
giles 4f02f82f4e HS parser: fix number+comparison keyword collision, eval-hs uses hs-compile
Parser: skip unit suffix when next ident is a comparison keyword
(starts, ends, contains, matches, is, does, in, precedes, follows).
Fixes "123 starts with '12'" returning "123starts" instead of true.

eval-hs: use hs-compile directly instead of hs-to-sx-from-source with
"return " prefix, which was causing the parser to consume the comparison
as a string suffix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:29:01 +00:00

128 lines
18 KiB
Plaintext

;; ---------------------------------------------------------------------------
;; Art DAG on SX — SX endpoints as portals into media processing environments
;; ---------------------------------------------------------------------------
(defcomp ()
(~docs/page :title "Art DAG on SX"
(p (~tw :tokens "text-stone-500 text-sm italic mb-8")
"An SX endpoint is a portal into an execution environment. The URL is the entry point; the boundary declaration determines what world you enter.")
;; =====================================================================
;; I. The endpoint as portal
;; =====================================================================
(~docs/section :title "The endpoint as portal" :id "endpoint-as-portal"
(p "An SX endpoint isn't a static route handler. It's a portal into an execution environment. A blog endpoint has " (code "query-posts") ", " (code "render-markdown") ". An art-dag endpoint has " (code "fetch-recipe") ", " (code "resolve-cid") ", " (code "gpu-exec") ", " (code "encode-stream") ", " (code "open-feed") ". Same evaluator. Different primitives. Different capabilities.")
(p "The URL is the entry point. The boundary declaration determines what world you enter. When you hit " (code "/art/render/Qm...") ", the evaluator boots with media-processing primitives. When you hit " (code "/blog/post/hello") ", the evaluator boots with content primitives. The SX code looks the same either way " (em "- ") "function calls, let bindings, conditionals. But the set of primitives available changes everything about what the program can do.")
(p "This is the same principle as the browser/server split. The browser has " (code "render-to-dom") " and " (code "signal") ". The server has " (code "query-db") " and " (code "render-to-html") ". Neither is a restricted version of the other " (em "- ") "they are different environments with different capabilities. The art-dag environment is a third world: " (code "gpu-exec") ", " (code "resolve-cid") ", " (code "encode-stream") ". Same language. Different universe."))
;; =====================================================================
;; II. Recipes as SX
;; =====================================================================
(~docs/section :title "Recipes as SX" :id "recipes-as-sx"
(p "Art DAG recipes are already s-expression effect chains. Currently executed by L1 Celery workers via Python. The SX version: recipes " (em "are") " SX programs. They evaluate in an environment that has media processing primitives. A recipe doesn't \"call\" a GPU function " (em "- ") "it evaluates in an environment where " (code "gpu-exec") " is a primitive.")
(~docs/code :src (highlight ";; A recipe is an SX program in a media-processing environment\n(define composite-layers\n (fn (base-cid overlay-cid blend)\n (let ((base (resolve-cid base-cid))\n (overlay (resolve-cid overlay-cid)))\n (gpu-exec :op \"composite\"\n :layers (list base overlay)\n :blend blend\n :output :stream))))" "lisp"))
(p "This isn't a DSL embedded in Python. It's SX, the same language that renders pages and defines components. The recipe author uses " (code "let") ", " (code "fn") ", " (code "map") ", " (code "if") " " (em "- ") "the full language. The only difference is what primitives are available. " (code "resolve-cid") " fetches content-addressed data. " (code "gpu-exec") " dispatches GPU operations. These are primitives, not library calls. They exist in the environment the same way " (code "+") " and " (code "str") " exist.")
(p "The recipe is data. It's an s-expression. You can parse it, analyze it, transform it, serialize it, hash it, store it, transmit it. You can inspect a recipe's dependency graph the same way " (code "deps.sx") " inspects component dependencies. You can type-check a recipe the same way " (code "typed-sx") " type-checks components. The tools already exist. They just need a new set of primitives to reason about."))
;; =====================================================================
;; III. Split execution
;; =====================================================================
(~docs/section :title "Split execution" :id "split-execution"
(p "Some recipe steps run against cached data (fast, local). Others need GPU. Others need live input. The evaluator doesn't dispatch " (em "- ") "the boundary declarations do. " (code "(with-boundary (gpu-compute) ...)") " migrates to a GPU-capable host. " (code "(with-boundary (live-ingest) ...)") " opens WebRTC feeds. The recipe author doesn't manage infrastructure " (em "- ") "they declare capabilities, and execution flows to where they exist.")
(~docs/code :src (highlight "(define live-composite\n (fn (recipe-cid camera-count)\n (let ((recipe (resolve-cid recipe-cid)))\n ;; Phase 1: cached data (local, fast)\n (let ((base-layers (map resolve-cid (get recipe \"layers\"))))\n ;; Phase 2: GPU processing\n (with-boundary (gpu-compute)\n (let ((composed (gpu-exec :op \"composite\"\n :layers base-layers\n :blend (get recipe \"blend\"))))\n ;; Phase 3: live feeds\n (with-boundary (live-ingest)\n (let ((feeds (map (fn (i)\n (open-feed :protocol \"webrtc\"\n :label (str \"camera-\" i)))\n (range 0 camera-count))))\n ;; Phase 4: encode and stream\n (with-boundary (encoding)\n (encode-stream\n :sources (concat (list composed) feeds)\n :codec \"h264\"\n :output :stream))))))))))" "lisp"))
(p "Four phases. Four capability requirements. The program reads linearly " (em "- ") "resolve cached layers, composite on GPU, open live feeds, encode output. But execution migrates across hosts as needed. The " (code "with-boundary") " blocks are the seams. Everything inside a boundary block runs on a host that provides those capabilities. Everything outside runs wherever the program started.")
(p "This is the same mechanism described in the generative SX plan's environment migration section. " (code "with-boundary") " serializes the environment (" (code "env-snapshot") "), ships the pending expression to a capable host, and execution continues there. The recipe author writes a linear program. The runtime makes it distributed."))
;; =====================================================================
;; IV. Content-addressed everything
;; =====================================================================
(~docs/section :title "Content-addressed everything" :id "content-addressed"
(p "Recipes are CIDs. Effects are CIDs. Intermediate frames are content-addressed. The execution DAG is a content-addressed graph " (em "- ") "every step can be verified, cached, or replayed.")
(p "A composite of three layers with a specific blend mode always produces the same CID. Caching is automatic: if the CID exists locally, skip the computation. This is the Art DAG's existing model " (em "- ") "SHA3-256 hashes identify everything. SX makes it explicit: the recipe source itself is content-addressed. Two users who write the same recipe get the same CID. They're running the same program.")
(div (~tw :tokens "rounded border border-stone-200 bg-stone-50 p-4 my-4")
(p (~tw :tokens "text-stone-700 font-medium mb-2") "Content addressing as memoization")
(p (~tw :tokens "text-stone-600 text-sm") "Every function call with content-addressed inputs has a content-addressed output. " (code "(gpu-exec :op \"composite\" :layers (list CID-a CID-b) :blend \"multiply\")") " always produces the same result CID. The runtime can check: does this output CID exist? If yes, skip the computation. The entire execution DAG becomes a cache key. Rerunning a recipe that's already been computed is instantaneous " (em "- ") "every intermediate result already exists."))
(p "The execution trace is also content-addressed. You can inspect exactly what happened: which CIDs were resolved, which GPU operations ran, which feeds were opened, what the final output was. The trace is the recipe's proof of work. It's immutable, verifiable, and shareable."))
;; =====================================================================
;; V. Feed generation
;; =====================================================================
(~docs/section :title "Feed generation" :id "feed-generation"
(p "The executing program creates new endpoints as a side effect. " (code "(open-feed ...)") " doesn't return data " (em "- ") "it returns a connectable endpoint. WebRTC peers, SSE streams, WebSocket channels. These are generative acts: the endpoint didn't exist before the recipe ran. The program grew its own input surface.")
(p "When the recipe completes or the island disposes, feeds are cleaned up via the disposal mechanism. An " (code "effect") " in island scope that opens a feed returns a cleanup function. When the island unmounts, the cleanup runs, the feed closes, the endpoint disappears. The lifecycle is automatic.")
(~docs/code :src (highlight ";; A feed is a connectable endpoint, not raw data\n(define create-camera-mosaic\n (fn (camera-ids)\n ;; Each open-feed returns a connectable URL, not bytes\n (let ((feeds (map (fn (id)\n (open-feed :protocol \"webrtc\"\n :label (str \"cam-\" id)\n :quality \"720p\"))\n camera-ids)))\n ;; The mosaic recipe composes feeds as inputs\n (gpu-exec :op \"mosaic\"\n :inputs feeds\n :layout \"grid\"\n :output (open-feed :protocol \"sse\"\n :label \"mosaic-output\"\n :format \"mjpeg\")))))" "lisp"))
(p "The output is itself a feed. A client connects to the mosaic output URL and receives composed frames. The feeds are the program's I/O surface " (em "- ") "they exist because the program created them, and they die when the program stops. No static route configuration. No service mesh. The program declares what it needs and creates what it produces."))
;; =====================================================================
;; VI. The client boundary
;; =====================================================================
(~docs/section :title "The client boundary" :id "client-boundary"
(p "The browser is just another execution environment with its own primitive set: " (code "render-to-dom") ", " (code "signal") ", " (code "deref") ", " (code "open-feed") " (as WebRTC consumer). A streaming art-dag response arrives as SX wire format. The client evaluates it in island scope " (em "- ") "signals bind to stream frames, " (code "reactive-list") " renders feed thumbnails, " (code "computed") " derives overlay parameters. The server pushes frames; the client renders them reactively. No special video player " (em "- ") "just signals and DOM.")
(~docs/code :src (highlight "(defisland ~plans/art-dag-sx/live-canvas ()\n (let ((frames (signal nil))\n (feed-url (signal nil)))\n ;; Connect to stream when URL arrives\n (effect (fn ()\n (when (deref feed-url)\n (connect-stream (deref feed-url)\n :on-frame (fn (f) (reset! frames f))))))\n (div :class \"relative aspect-video bg-black rounded\"\n (when (deref frames)\n (canvas :width 1920 :height 1080\n :draw (fn (ctx)\n (draw-frame ctx (deref frames)))))\n (when (not (deref frames))\n (p :class \"absolute inset-0 flex items-center justify-center text-white/50\"\n \"Waiting for stream...\")))))" "lisp"))
(p "The island is reactive. When " (code "frames") " updates, only the canvas redraws. When " (code "feed-url") " updates, the effect reconnects. No polling loop. No WebSocket message handler parsing JSON. The stream is a signal source. The DOM is a signal consumer. The reactive graph connects them.")
(p "This is the same island architecture from the reactive islands plan " (em "- ") "signals, effects, computed, disposal. The only difference is the data source. Instead of an HTMX response mutating a signal, a WebRTC stream mutates a signal. The rendering pipeline doesn't know or care where the data comes from. It reacts."))
;; =====================================================================
;; VII. L1/L2 integration
;; =====================================================================
(~docs/section :title "L1/L2 integration" :id "l1-l2"
(p "L1 is the compute layer (Celery workers, GPU nodes). L2 is the registry (ActivityPub, recipe discovery). In SX terms: L1 hosts provide " (code "gpu-exec") ", " (code "encode-stream") ", " (code "resolve-cid-local") ". L2 hosts provide " (code "discover-recipe") ", " (code "publish-recipe") ", " (code "federate-activity") ".")
(p "An SX program that needs both crosses boundaries as needed " (em "- ") "fetch recipe metadata from L2, execute it on L1, publish results back to L2.")
(~docs/code :src (highlight ";; A full pipeline crossing L1 and L2 boundaries\n(define render-and-publish\n (fn (recipe-name output-label)\n ;; L2: discover the recipe\n (with-boundary (registry)\n (let ((recipe-cid (discover-recipe :name recipe-name\n :version \"latest\")))\n ;; L1: execute the recipe\n (with-boundary (gpu-compute)\n (let ((result-cid (gpu-exec :recipe (resolve-cid recipe-cid)\n :output :cid)))\n ;; L2: publish the result\n (with-boundary (registry)\n (publish-recipe\n :name output-label\n :input-cid recipe-cid\n :output-cid result-cid\n :activity \"Create\"))))))))" "lisp"))
(p "Three boundary crossings. L2 to find the recipe. L1 to execute it. L2 to publish the result. The program reads as a linear sequence of operations. The runtime handles the dispatch " (em "- ") "which host provides " (code "discover-recipe") ", which host provides " (code "gpu-exec") ", which host provides " (code "publish-recipe") ". The program author doesn't configure endpoints or manage connections. They declare capabilities."))
;; =====================================================================
;; VIII. The primitive sets
;; =====================================================================
(~docs/section :title "The primitive sets" :id "primitive-sets"
(p "Each execution environment provides its own set of primitives. The language is the same everywhere. The capabilities differ.")
(div (~tw :tokens "overflow-x-auto rounded border border-stone-200 mb-4")
(table (~tw :tokens "w-full text-left text-sm")
(thead (tr (~tw :tokens "border-b border-stone-200 bg-stone-100")
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Environment")
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Primitives")
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Runs on")))
(tbody
(tr (~tw :tokens "border-b border-stone-100")
(td (~tw :tokens "px-3 py-2 font-medium text-stone-700") "Browser")
(td (~tw :tokens "px-3 py-2 font-mono text-xs text-stone-600") "render-to-dom, signal, deref, connect-stream")
(td (~tw :tokens "px-3 py-2 text-stone-600") "Client"))
(tr (~tw :tokens "border-b border-stone-100")
(td (~tw :tokens "px-3 py-2 font-medium text-stone-700") "App server")
(td (~tw :tokens "px-3 py-2 font-mono text-xs text-stone-600") "query-db, render-to-html, fetch-fragment")
(td (~tw :tokens "px-3 py-2 text-stone-600") "Quart service"))
(tr (~tw :tokens "border-b border-stone-100")
(td (~tw :tokens "px-3 py-2 font-medium text-stone-700") "L1 Worker")
(td (~tw :tokens "px-3 py-2 font-mono text-xs text-stone-600") "gpu-exec, resolve-cid, encode-stream, cache-put")
(td (~tw :tokens "px-3 py-2 text-stone-600") "Celery + GPU"))
(tr (~tw :tokens "border-b border-stone-100")
(td (~tw :tokens "px-3 py-2 font-medium text-stone-700") "L2 Registry")
(td (~tw :tokens "px-3 py-2 font-mono text-xs text-stone-600") "discover-recipe, publish-recipe, federate")
(td (~tw :tokens "px-3 py-2 text-stone-600") "FastAPI"))
(tr (~tw :tokens "border-b border-stone-100")
(td (~tw :tokens "px-3 py-2 font-medium text-stone-700") "Live Ingest")
(td (~tw :tokens "px-3 py-2 font-mono text-xs text-stone-600") "open-feed, capture-frame, transcode")
(td (~tw :tokens "px-3 py-2 text-stone-600") "WebRTC gateway"))
(tr
(td (~tw :tokens "px-3 py-2 font-medium text-stone-700") "IPFS Node")
(td (~tw :tokens "px-3 py-2 font-mono text-xs text-stone-600") "pin-cid, resolve-cid, dag-put, dag-get")
(td (~tw :tokens "px-3 py-2 text-stone-600") "Kubo")))))
(p "A pure SX program (no IO primitives) runs on all six. A program that calls " (code "gpu-exec") " runs on L1 workers. A program that calls " (code "render-to-dom") " runs in the browser. The boundary declaration is the type signature of the environment. It tells you where the program can execute.")
(p "Adding a new environment means declaring a new primitive set. A hypothetical audio-processing environment would provide " (code "mix-tracks") ", " (code "apply-effect") ", " (code "encode-audio") ". A program that uses those primitives runs wherever that environment is hosted. The language doesn't change. The evaluator doesn't change. Only the available primitives change.")
(div (~tw :tokens "rounded border border-violet-200 bg-violet-50 p-4 my-4")
(p (~tw :tokens "text-violet-900 font-medium") "One language, many worlds")
(p (~tw :tokens "text-violet-800 text-sm") "The art-dag integration isn't a new feature bolted onto SX. It's a demonstration of what SX already is: a language where the execution environment is parameterized by its primitive set. The browser, the app server, the GPU worker, and the IPFS node all run the same evaluator. They differ only in what primitives they provide. The art-dag is just another world you can enter through an endpoint.")))
))