All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m50s
Replace path-based URLs with nested s-expression URLs across the sx app. URLs like /language/docs/introduction become /(language.(doc.introduction)), making the URL simultaneously a query, render instruction, and address. - Add sx_router.py: catch-all route evaluator with dot→space conversion, auto-quoting slugs, two-phase eval, streaming detection, 301 redirects - Add page-functions.sx: section + page functions for URL dispatch - Rewrite nav-data.sx: ~200 hrefs to SX expression format, tree-descent nav matching via has-descendant-href? (replaces prefix heuristics) - Convert ~120 old-style hrefs across 26 .sx content files - Add SX Protocol proposal (etc/plans/sx-protocol) - Wire catch-all route in app.py with before_request redirect handler Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
198 lines
12 KiB
Plaintext
198 lines
12 KiB
Plaintext
;; SX Protocol — A Proposal
|
|
;; S-expressions as a universal protocol for networked hypermedia.
|
|
|
|
(defcomp ~plan-sx-protocol-content ()
|
|
(~doc-page :title "SX Protocol — A Proposal"
|
|
|
|
(~doc-section :title "Abstract" :id "abstract"
|
|
(p "SX is a Lisp dialect and a proposed universal protocol for networked hypermedia. "
|
|
"It replaces URLs, HTTP verbs, query strings, API query languages, and rendering layers "
|
|
"with a single unified concept: " (strong "the s-expression") ".")
|
|
(p "Everything is an expression. Everything is evaluable. Everything is composable."))
|
|
|
|
(~doc-section :title "The Problem With the Current Web" :id "problem"
|
|
(p "The modern web stack has accumulated layers of incompatible syntax to express "
|
|
"what are fundamentally the same things:")
|
|
(table :class "w-full text-sm border-collapse mb-4"
|
|
(thead
|
|
(tr :class "border-b border-stone-300"
|
|
(th :class "text-left py-2 pr-4" "Concern")
|
|
(th :class "text-left py-2 pr-4" "Current Syntax")
|
|
(th :class "text-left py-2" "Example")))
|
|
(tbody
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "py-2 pr-4" "Resource path")
|
|
(td :class "py-2 pr-4" "URL segments")
|
|
(td :class "py-2 font-mono text-xs" "/users/123/posts"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "py-2 pr-4" "Query parameters")
|
|
(td :class "py-2 pr-4 font-mono text-xs" "?key=value")
|
|
(td :class "py-2 font-mono text-xs" "?filter=published&sort=date"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "py-2 pr-4" "API queries")
|
|
(td :class "py-2 pr-4" "GraphQL / REST")
|
|
(td :class "py-2 font-mono text-xs" "{ posts(filter: \"published\") { title } }"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "py-2 pr-4" "Network verb")
|
|
(td :class "py-2 pr-4" "HTTP method")
|
|
(td :class "py-2 font-mono text-xs" "GET, POST, PUT"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "py-2 pr-4" "Real-time")
|
|
(td :class "py-2 pr-4" "WebSocket URL")
|
|
(td :class "py-2 font-mono text-xs" "wss://site.com/live"))
|
|
(tr
|
|
(td :class "py-2 pr-4" "Rendering")
|
|
(td :class "py-2 pr-4" "HTML + CSS + JS")
|
|
(td :class "py-2" "Three separate languages"))))
|
|
(p "Each layer invented its own syntax. None of them compose. None of them are executable. "
|
|
"None of them are data."))
|
|
|
|
(~doc-section :title "The SX Approach" :id "approach"
|
|
|
|
(h3 :class "text-lg font-semibold mt-6 mb-2" "URLs as S-Expressions")
|
|
(p "A conventional URL:")
|
|
(~doc-code :code (highlight "https://site.com/blog/my-post?filter=published&sort=date" "text"))
|
|
(p "As an SX expression:")
|
|
(~doc-code :code (highlight "(get.site.com.(blog.(my-post.(filter.published.sort.date))))" "lisp"))
|
|
(ul
|
|
(li "The protocol/verb is the first atom: " (code "get"))
|
|
(li "The domain follows: " (code "site.com"))
|
|
(li "Path and parameters collapse into " (strong "one unified nested structure"))
|
|
(li "No " (code "?") ", no " (code "&") ", no " (code "/") " — just lists"))
|
|
|
|
(h3 :class "text-lg font-semibold mt-6 mb-2" "Dots, Not Spaces")
|
|
(p "Lisp conventionally uses spaces as separators. In URLs, spaces become " (code "%20")
|
|
". SX uses dots instead, which are URL-safe and semantically meaningful — a dot between "
|
|
"two atoms is a " (strong "cons pair") ", the fundamental unit of Lisp structure.")
|
|
(~doc-code :code (highlight ";; Clean, URL-safe, valid Lisp\n(blog.(filter.published).(sort.date.desc))" "lisp"))
|
|
|
|
(h3 :class "text-lg font-semibold mt-6 mb-2" "Verbs Are Just Atoms")
|
|
(p "HTTP methods are not special syntax — they are simply the first element of the expression:")
|
|
(~doc-code :code (highlight "(get.site.com.(post.my-first-post)) ; read\n(post.site.com.(submit-post.(title.hello))) ; write\n(ws.site.com.(live-feed)) ; websocket / subscribe" "lisp"))
|
|
(p "No special protocol prefixes. No " (code "https://") " vs " (code "wss://")
|
|
". The verb is data, like everything else."))
|
|
|
|
(~doc-section :title "Graph-SX: Hypermedia Queries" :id "graph-sx"
|
|
(p "GraphQL was a major advance over REST, but it made two compromises:")
|
|
(ol
|
|
(li "Queries are sent as POST bodies, sacrificing cacheability and shareability")
|
|
(li "Responses are dead data — JSON that must be separately rendered"))
|
|
(p (strong "Graph-SX") " addresses both.")
|
|
|
|
(h3 :class "text-lg font-semibold mt-6 mb-2" "Queries Are URLs")
|
|
(p "Because SX expressions are URLs, every query is a GET request:")
|
|
(~doc-code :code (highlight ";; This is a URL and a query simultaneously\n(get.site.com.(blog.(filter.(tag.lisp)).(limit.10)))" "lisp"))
|
|
(ul
|
|
(li "Fully cacheable by CDNs")
|
|
(li "Bookmarkable and shareable")
|
|
(li "No POST body required for reads")
|
|
(li "Browser back button works correctly"))
|
|
|
|
(h3 :class "text-lg font-semibold mt-6 mb-2" "Responses Include Rendering")
|
|
(p "GraphQL returns data. Graph-SX returns " (strong "hypermedia")
|
|
" — data and its presentation in the same expression:")
|
|
(~doc-code :code (highlight ";; GraphQL response (dead data)\n{\"title\": \"My Post\", \"body\": \"Hello world\"}\n\n;; Graph-SX response (live hypermedia)\n(article\n (h1 \"My Post\")\n (p \"Hello world\")\n (a (href (get.site.com.(post.next-post))) \"Next\"))" "lisp"))
|
|
(p "The server returns what the resource " (strong "is") " and how to "
|
|
(strong "present") " it in one unified structure. There is no separate rendering layer.")
|
|
|
|
(h3 :class "text-lg font-semibold mt-6 mb-2" "Queries Are Transformations")
|
|
(p "Because SX is a full programming language, the query and the transformation "
|
|
"are the same expression:")
|
|
(~doc-code :code (highlight ";; Fetch, filter, and transform in one expression\n(map (lambda (p) (title p))\n (filter published?\n (posts (after \"2025\"))))" "lisp"))
|
|
(p "No separate processing step. No client-side data manipulation layer."))
|
|
|
|
(~doc-section :title "Components" :id "components"
|
|
(p "SX supports server-side composable components via the " (code "~") " prefix convention:")
|
|
(~doc-code :code (highlight "(~get.everything-under-the-sun)" "lisp"))
|
|
(p "A " (code "~component") " is a named server-side function that:")
|
|
(ol
|
|
(li "Receives the expression as arguments")
|
|
(li "Makes onward queries as needed")
|
|
(li "Processes and composes results")
|
|
(li "Returns hypermedia"))
|
|
(p "Components compose naturally:")
|
|
(~doc-code :code (highlight "(~page.home\n (~hero.banner)\n (~get.latest-posts.(limit.5))\n (~get.featured.(filter.pinned)))" "lisp"))
|
|
(p "This is equivalent to React Server Components — but without a framework, "
|
|
"without a build step, and without leaving Lisp."))
|
|
|
|
(~doc-section :title "Cross-Domain Composition" :id "cross-domain"
|
|
(p "Because domain and verb are just atoms, cross-domain queries are structurally "
|
|
"identical to local ones:")
|
|
(~doc-code :code (highlight ";; Local\n(post.my-first-post)\n\n;; Remote — identical structure, qualified\n(get.site.com.(post.my-first-post))\n\n;; Composed across domains\n(~render\n (get.site.com.(post.my-first-post))\n (get.cdn.com.(image.hero)))" "lisp"))
|
|
(p "Network calls are function calls. Remote resources are just namespaced expressions."))
|
|
|
|
(~doc-section :title "Self-Describing and Introspectable" :id "introspectable"
|
|
(p "Because the site is implemented in SX and served as SX, every page is introspectable:")
|
|
(~doc-code :code (highlight "(get.sx.dev.(about)) ; the about page\n(get.sx.dev.(source.(about))) ; the SX source for the about page\n(get.sx.dev.(eval.(source.about))) ; re-evaluate it live" "lisp"))
|
|
(p "The site is its own documentation. The source is always one expression away."))
|
|
|
|
(~doc-section :title "Comparison" :id "comparison"
|
|
(table :class "w-full text-sm border-collapse mb-4"
|
|
(thead
|
|
(tr :class "border-b border-stone-300"
|
|
(th :class "text-left py-2 pr-4" "Feature")
|
|
(th :class "text-left py-2 pr-4" "REST")
|
|
(th :class "text-left py-2 pr-4" "GraphQL")
|
|
(th :class "text-left py-2" "Graph-SX")))
|
|
(tbody
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "py-2 pr-4" "Reads are GETs")
|
|
(td :class "py-2 pr-4 text-green-700" "Yes")
|
|
(td :class "py-2 pr-4 text-red-700" "No (POST)")
|
|
(td :class "py-2 text-green-700" "Yes"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "py-2 pr-4" "CDN cacheable")
|
|
(td :class "py-2 pr-4 text-green-700" "Yes")
|
|
(td :class "py-2 pr-4 text-red-700" "No")
|
|
(td :class "py-2 text-green-700" "Yes"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "py-2 pr-4" "Nested queries")
|
|
(td :class "py-2 pr-4 text-red-700" "No")
|
|
(td :class "py-2 pr-4 text-green-700" "Yes")
|
|
(td :class "py-2 text-green-700" "Yes"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "py-2 pr-4" "Response includes rendering")
|
|
(td :class "py-2 pr-4 text-red-700" "No")
|
|
(td :class "py-2 pr-4 text-red-700" "No")
|
|
(td :class "py-2 text-green-700" "Yes"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "py-2 pr-4" "Query is a transformation")
|
|
(td :class "py-2 pr-4 text-red-700" "No")
|
|
(td :class "py-2 pr-4 text-red-700" "No")
|
|
(td :class "py-2 text-green-700" "Yes"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "py-2 pr-4" "Composable across domains")
|
|
(td :class "py-2 pr-4 text-red-700" "No")
|
|
(td :class "py-2 pr-4 text-red-700" "No")
|
|
(td :class "py-2 text-green-700" "Yes"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "py-2 pr-4" "One syntax for everything")
|
|
(td :class "py-2 pr-4 text-red-700" "No")
|
|
(td :class "py-2 pr-4 text-red-700" "No")
|
|
(td :class "py-2 text-green-700" "Yes"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "py-2 pr-4" "Bookmarkable deep links")
|
|
(td :class "py-2 pr-4 text-green-700" "Yes")
|
|
(td :class "py-2 pr-4 text-red-700" "No")
|
|
(td :class "py-2 text-green-700" "Yes"))
|
|
(tr
|
|
(td :class "py-2 pr-4" "Self-hosting / introspectable")
|
|
(td :class "py-2 pr-4 text-red-700" "No")
|
|
(td :class "py-2 pr-4 text-stone-500" "Partial")
|
|
(td :class "py-2 text-green-700" "Yes")))))
|
|
|
|
(~doc-section :title "Future Direction" :id "future"
|
|
(p "The logical conclusion of SX is a " (strong "new internet protocol")
|
|
" in which the URL, the HTTP verb, the query language, the response format, "
|
|
"and the rendering layer are all unified under one evaluable expression format.")
|
|
(~doc-code :code (highlight ";; The entire network request — protocol, domain, verb, query, all one expression\n(get.sx.dev.(blog.(filter.(tag.lisp)).(limit.10)))" "lisp"))
|
|
(p "HTTP becomes one possible implementation of a more general principle:")
|
|
(blockquote :class "border-l-4 border-violet-300 pl-4 italic text-stone-600 my-4"
|
|
(p (strong "Evaluate this expression. Return an expression."))))
|
|
|
|
(~doc-section :title "Reference Implementation" :id "reference"
|
|
(p "SX is implemented in SX. The reference implementation is self-hosting and available at:")
|
|
(~doc-code :code (highlight "(get.sx.dev.(source.evaluator))" "lisp"))
|
|
(p :class "text-sm text-stone-500 mt-4 italic"
|
|
"This proposal was written in conversation with Claude (Anthropic). The ideas are the author's own."))))
|