Step 1 — defhelper: SX-defined page data helpers replace Python helpers. (defhelper name (params) body) in .sx files, using existing IO primitives (query, action, service). Loaded into OCaml kernel as pure SX defines. Step 2 — SX config: app-config.sx replaces app-config.yaml with (defconfig) form. (env-get "VAR") resolves secrets from environment. Kebab-to-underscore aliasing ensures backward compatibility with all 174 config consumers. Also: SXTP protocol spec (applications/sxtp/spec.sx), docs article, sx_nav move/delete modes, reactive-runtime moved to geography. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
341 lines
16 KiB
Plaintext
341 lines
16 KiB
Plaintext
(defcomp
|
|
~applications/sxtp/content
|
|
()
|
|
(~docs/page
|
|
:title "SXTP Protocol"
|
|
(~docs/section
|
|
:title "Overview"
|
|
:id "overview"
|
|
(p
|
|
"SXTP — SX Transfer Protocol — is HTTP reimagined where the wire format "
|
|
(em "is")
|
|
" the language. Requests, responses, headers, cookies, status conditions, and bodies are all s-expressions. There is no text framing, no content-type negotiation, no URL query-string encoding.")
|
|
(p "Design principles:")
|
|
(ul
|
|
:class "list-disc list-inside space-y-2 mt-2"
|
|
(li
|
|
(strong "SX all the way")
|
|
" — every datum on the wire is a valid SX value")
|
|
(li
|
|
(strong "Open verb set")
|
|
" — any symbol is a legal verb, not just GET/POST/PUT/DELETE")
|
|
(li
|
|
(strong "Structured metadata")
|
|
" — headers and cookies are dicts, not flat strings")
|
|
(li
|
|
(strong "Capability-scoped")
|
|
" — requests declare required capabilities")
|
|
(li
|
|
(strong "Content-addressed")
|
|
" — responses can be cached by hash")
|
|
(li
|
|
(strong "Streamable")
|
|
" — chunked responses are sequences of expressions")))
|
|
(~docs/section
|
|
:title "Requests"
|
|
:id "requests"
|
|
(p
|
|
"A request is a list beginning with the symbol "
|
|
(code "request")
|
|
". All fields are keyword arguments.")
|
|
(~docs/code
|
|
:src (highlight "(request :verb navigate :path \"/\")" "lisp"))
|
|
(p "Full request with all fields:")
|
|
(~docs/code
|
|
:src (highlight
|
|
"(request\n :verb navigate\n :path \"/geography/capabilities\"\n :headers {:accept \"text/sx\" :language \"en\"}\n :cookies {:session \"tok_abc123\" :prefs {:theme \"dark\"}}\n :params {:page 1 :per-page 20}\n :capabilities (fetch query)\n :body nil)"
|
|
"lisp"))
|
|
(div
|
|
:class "overflow-x-auto rounded border border-stone-200 mt-4"
|
|
(table
|
|
:class "w-full text-left text-sm"
|
|
(thead
|
|
(tr
|
|
:class "border-b border-stone-200 bg-stone-100"
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Field")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Description")))
|
|
(tbody
|
|
(tr
|
|
:class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700 font-mono" ":verb")
|
|
(td
|
|
:class "px-3 py-2 text-stone-600"
|
|
"Symbol — the action to perform (required)"))
|
|
(tr
|
|
:class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700 font-mono" ":path")
|
|
(td
|
|
:class "px-3 py-2 text-stone-600"
|
|
"String — resource path (required)"))
|
|
(tr
|
|
:class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700 font-mono" ":headers")
|
|
(td
|
|
:class "px-3 py-2 text-stone-600"
|
|
"Dict — structured request metadata"))
|
|
(tr
|
|
:class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700 font-mono" ":cookies")
|
|
(td
|
|
:class "px-3 py-2 text-stone-600"
|
|
"Dict — client state, values can be any SX type"))
|
|
(tr
|
|
:class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700 font-mono" ":params")
|
|
(td
|
|
:class "px-3 py-2 text-stone-600"
|
|
"Dict — query parameters as typed values"))
|
|
(tr
|
|
:class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700 font-mono" ":capabilities")
|
|
(td
|
|
:class "px-3 py-2 text-stone-600"
|
|
"List — capabilities this request requires"))
|
|
(tr
|
|
(td :class "px-3 py-2 text-stone-700 font-mono" ":body")
|
|
(td
|
|
:class "px-3 py-2 text-stone-600"
|
|
"Any SX value — request payload"))))))
|
|
(~docs/section
|
|
:title "Responses"
|
|
:id "responses"
|
|
(p
|
|
"A response is a list beginning with the symbol "
|
|
(code "response")
|
|
".")
|
|
(~docs/code
|
|
:src (highlight
|
|
"(response :status ok\n :headers {:content-type \"text/sx\" :cache :immutable}\n :set-cookie {:session {:value \"tok_xyz\" :max-age 3600 :path \"/\"}}\n :body (page :title \"Home\" (h1 \"Welcome\")))"
|
|
"lisp"))
|
|
(p
|
|
"The body isn't serialized HTML that needs parsing — it's a live component tree the browser evaluates directly."))
|
|
(~docs/section
|
|
:title "Verbs"
|
|
:id "verbs"
|
|
(p
|
|
"Unlike HTTP's fixed set, any symbol is a valid verb. Convention defines common verbs; domains add their own.")
|
|
(div
|
|
:class "overflow-x-auto rounded border border-stone-200 mt-4"
|
|
(table
|
|
:class "w-full text-left text-sm"
|
|
(thead
|
|
(tr
|
|
:class "border-b border-stone-200 bg-stone-100"
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Verb")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Purpose")))
|
|
(tbody
|
|
(tr
|
|
:class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700 font-mono" "navigate")
|
|
(td
|
|
:class "px-3 py-2 text-stone-600"
|
|
"Retrieve a page for display — analogous to GET for documents"))
|
|
(tr
|
|
:class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700 font-mono" "fetch")
|
|
(td
|
|
:class "px-3 py-2 text-stone-600"
|
|
"Retrieve data — analogous to GET for APIs"))
|
|
(tr
|
|
:class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700 font-mono" "query")
|
|
(td
|
|
:class "px-3 py-2 text-stone-600"
|
|
"Structured query — body contains a query expression"))
|
|
(tr
|
|
:class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700 font-mono" "mutate")
|
|
(td
|
|
:class "px-3 py-2 text-stone-600"
|
|
"Change state — analogous to POST/PUT/PATCH"))
|
|
(tr
|
|
:class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700 font-mono" "create")
|
|
(td :class "px-3 py-2 text-stone-600" "Create a new resource"))
|
|
(tr
|
|
:class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700 font-mono" "delete")
|
|
(td :class "px-3 py-2 text-stone-600" "Remove a resource"))
|
|
(tr
|
|
:class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700 font-mono" "subscribe")
|
|
(td
|
|
:class "px-3 py-2 text-stone-600"
|
|
"Open a streaming channel for real-time updates"))
|
|
(tr
|
|
:class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700 font-mono" "inspect")
|
|
(td
|
|
:class "px-3 py-2 text-stone-600"
|
|
"Retrieve metadata about a resource (capabilities, schema)"))
|
|
(tr
|
|
(td :class "px-3 py-2 text-stone-700 font-mono" "ping")
|
|
(td :class "px-3 py-2 text-stone-600" "Liveness check")))))
|
|
(p :class "mt-4" "Domains define their own verbs freely:")
|
|
(~docs/code
|
|
:src (highlight
|
|
"(request :verb publish :path \"/blog/draft-123\")\n(request :verb checkout :path \"/cart\")\n(request :verb render :path \"/artdag/node/abc\" :params {:format \"png\"})\n(request :verb federate :path \"/outbox\" :body (activity ...))"
|
|
"lisp")))
|
|
(~docs/section
|
|
:title "What HTTP got wrong"
|
|
:id "http-comparison"
|
|
(div
|
|
:class "overflow-x-auto rounded border border-stone-200"
|
|
(table
|
|
:class "w-full text-left text-sm"
|
|
(thead
|
|
(tr
|
|
:class "border-b border-stone-200 bg-stone-100"
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "HTTP pain")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "SXTP answer")))
|
|
(tbody
|
|
(tr
|
|
:class "border-b border-stone-100"
|
|
(td
|
|
:class "px-3 py-2 text-stone-700"
|
|
"Fixed verb set (GET/POST/PUT/DELETE)")
|
|
(td :class "px-3 py-2 text-stone-600" "Any symbol is a verb"))
|
|
(tr
|
|
:class "border-b border-stone-100"
|
|
(td
|
|
:class "px-3 py-2 text-stone-700"
|
|
"Headers are flat string pairs")
|
|
(td
|
|
:class "px-3 py-2 text-stone-600"
|
|
"Headers are dicts — nested, typed"))
|
|
(tr
|
|
:class "border-b border-stone-100"
|
|
(td
|
|
:class "px-3 py-2 text-stone-700"
|
|
"Cookies are encoded strings")
|
|
(td :class "px-3 py-2 text-stone-600" "Cookies are SX values"))
|
|
(tr
|
|
:class "border-b border-stone-100"
|
|
(td
|
|
:class "px-3 py-2 text-stone-700"
|
|
"Body requires content-type negotiation")
|
|
(td
|
|
:class "px-3 py-2 text-stone-600"
|
|
"Body is always SX — rendering is the client's job"))
|
|
(tr
|
|
:class "border-b border-stone-100"
|
|
(td
|
|
:class "px-3 py-2 text-stone-700"
|
|
"URL query strings (?a=1&b=2)")
|
|
(td
|
|
:class "px-3 py-2 text-stone-600"
|
|
"Params are part of the request expression"))
|
|
(tr
|
|
(td
|
|
:class "px-3 py-2 text-stone-700"
|
|
"Separate mechanisms for streaming")
|
|
(td
|
|
:class "px-3 py-2 text-stone-600"
|
|
"Streaming is just :stream true + chunk sequences"))))))
|
|
(~docs/section
|
|
:title "Status and conditions"
|
|
:id "status"
|
|
(p
|
|
"Status is a symbol, not a number. Conditions replace error codes with structured, informative values.")
|
|
(~docs/code
|
|
:src (highlight
|
|
"(response :status not-found\n :body (condition :type resource-not-found\n :path \"/blog/nonexistent\"\n :message \"No such post\"\n :retry false))"
|
|
"lisp"))
|
|
(p "Conditions are extensible — domains define their own:")
|
|
(~docs/code
|
|
:src (highlight
|
|
"(condition :type payment-declined\n :reason :insufficient-funds\n :provider \"sumup\")"
|
|
"lisp")))
|
|
(~docs/section
|
|
:title "Streaming"
|
|
:id "streaming"
|
|
(p
|
|
"A streaming response sets "
|
|
(code ":stream true")
|
|
". The body becomes a sequence of chunk expressions.")
|
|
(~docs/code
|
|
:src (highlight
|
|
";; Ordered chunks\n(response :status ok :stream true)\n(chunk :seq 0 :body (tr (td \"Row 1\") (td \"data\")))\n(chunk :seq 1 :body (tr (td \"Row 2\") (td \"data\")))\n(chunk :done true)\n\n;; Server-sent events via subscribe\n(request :verb subscribe :path \"/events/live\")\n\n(event :type new-event :id \"evt-42\"\n :body (div :class \"event-card\" (h3 \"Jazz Night\")))\n(event :type update :id \"evt-42\"\n :body {:attendees 51})\n(event :type heartbeat :time 1711612800)"
|
|
"lisp")))
|
|
(~docs/section
|
|
:title "Capabilities"
|
|
:id "capabilities"
|
|
(p
|
|
"Requests declare the capabilities they need. The server checks these against the session's granted capabilities. Insufficient capabilities produce "
|
|
(code "(response :status forbidden)")
|
|
".")
|
|
(~docs/code
|
|
:src (highlight
|
|
";; Client declares\n(request :verb query :path \"/events\"\n :capabilities (fetch db:read))\n\n;; Server grants on auth\n(response :status ok\n :set-cookie {:capabilities {:value (fetch query db:read mutate)\n :max-age 86400\n :secure true}})"
|
|
"lisp"))
|
|
(p "Inspect what a resource requires:")
|
|
(~docs/code
|
|
:src (highlight
|
|
"(request :verb inspect :path \"/cart/checkout\")\n\n(response :status ok\n :body {:required-capabilities (mutate cart:checkout)\n :available-verbs (inspect mutate)\n :params-schema {:shipping-address \"dict\"\n :payment-method \"symbol\"}})"
|
|
"lisp")))
|
|
(~docs/section
|
|
:title "Caching"
|
|
:id "caching"
|
|
(p
|
|
"Content-addressed caching. The response hash "
|
|
(em "is")
|
|
" the cache key. No ETags, no Last-Modified — just SX content hashes.")
|
|
(~docs/code
|
|
:src (highlight
|
|
";; Server provides hash\n(response :status ok\n :headers {:content-hash \"sha3-abc123...\"\n :cache :immutable}\n :body ...)\n\n;; Client validates\n(request :verb fetch :path \"/geography/capabilities\"\n :headers {:if-match \"sha3-abc123...\"})\n\n(response :status not-modified)"
|
|
"lisp"))
|
|
(p
|
|
"Three cache policies: "
|
|
(code ":immutable")
|
|
" (content-addressed, never changes), "
|
|
(code ":revalidate")
|
|
" (check hash before using), "
|
|
(code ":none")
|
|
" (dynamic content)."))
|
|
(~docs/section
|
|
:title "Wire format"
|
|
:id "wire-format"
|
|
(p
|
|
"On the wire, each message is a length-prefixed SX expression. Length is a decimal integer as ASCII, followed by newline. The SX expression is UTF-8 encoded.")
|
|
(~docs/code
|
|
:src (highlight "43\n(request :verb ping :path \"/\" :body nil)" "text"))
|
|
(p
|
|
"Connections are persistent — multiple request/response pairs on the same connection. Pipelining is allowed. TLS is the transport security layer: "
|
|
(code "sxtp://")
|
|
" is plaintext (port 5380), "
|
|
(code "sxtps://")
|
|
" is TLS (port 5381)."))
|
|
(~docs/section
|
|
:title "URI scheme"
|
|
:id "uri"
|
|
(p "The browser translates URIs into request expressions:")
|
|
(~docs/code
|
|
:src (highlight
|
|
"sxtps://blog.rose-ash.com/geography/capabilities\n\n;; becomes\n\n(request :verb navigate\n :path \"/geography/capabilities\"\n :headers {:host \"blog.rose-ash.com\"})"
|
|
"lisp")))
|
|
(~docs/section
|
|
:title "Examples"
|
|
:id "examples"
|
|
(p "Page navigation:")
|
|
(~docs/code
|
|
:src (highlight
|
|
"(request :verb navigate :path \"/geography/capabilities\"\n :headers {:host \"sx.rose-ash.com\" :accept \"text/sx\"})\n\n(response :status ok\n :headers {:content-type \"text/sx\"\n :content-hash \"sha3-9f2a...\"}\n :body (page :title \"Capabilities\"\n (h1 \"Geography Capabilities\")\n (~capability-list :domain \"geography\")))"
|
|
"lisp"))
|
|
(p "Structured query:")
|
|
(~docs/code
|
|
:src (highlight
|
|
"(request :verb query :path \"/events\"\n :capabilities (fetch db:read)\n :params {:after \"2026-03-01\" :limit 10}\n :body (filter (events) (fn (e) (> (:attendees e) 50))))\n\n(response :status ok\n :headers {:cache :revalidate}\n :body ((event :id \"evt-42\" :title \"Jazz Night\" :attendees 87)\n (event :id \"evt-55\" :title \"Art Walk\" :attendees 120)))"
|
|
"lisp"))
|
|
(p "Creating a resource:")
|
|
(~docs/code
|
|
:src (highlight
|
|
"(request :verb create :path \"/blog/posts\"\n :capabilities (mutate blog:publish)\n :cookies {:session \"tok_abc123\"}\n :body {:title \"SXTP Protocol\"\n :body (article (h1 \"SXTP\") (p \"Everything is SX.\"))\n :tags (\"protocol\" \"sx\" \"web\")})\n\n(response :status created\n :headers {:location \"/blog/posts/sxtp-protocol\"\n :content-hash \"sha3-ff01...\"}\n :body {:id \"post-789\"\n :path \"/blog/posts/sxtp-protocol\"\n :created-at 1711612800})"
|
|
"lisp")))
|
|
(~docs/section
|
|
:title "Specification"
|
|
:id "spec"
|
|
(p
|
|
"The formal specification lives in "
|
|
(code "applications/sxtp/spec.sx")
|
|
" — a self-describing SX file where the field definitions are themselves SX data structures that the protocol can introspect."))))
|