Merge branch 'worktree-endpoints' into macros

This commit is contained in:
2026-03-11 23:48:30 +00:00
14 changed files with 756 additions and 256 deletions

View File

@@ -215,6 +215,56 @@ def create_handler_blueprint(service_name: str) -> Any:
return bp
# ---------------------------------------------------------------------------
# Public route registration — handlers with :path get mounted as routes
# ---------------------------------------------------------------------------
def register_route_handlers(app_or_bp: Any, service_name: str) -> int:
"""Register public routes for all handlers with :path defined.
Returns the number of routes registered.
"""
from quart import Response, request
from shared.browser.app.csrf import csrf_exempt
handlers = get_all_handlers(service_name)
count = 0
for name, hdef in handlers.items():
if not hdef.is_route:
continue
# Capture hdef in closure
_hdef = hdef
async def _route_view(_h=_hdef, **path_kwargs):
from shared.sx.helpers import sx_response
args = dict(request.args)
args.update(path_kwargs)
result = await execute_handler(_h, service_name, args=args)
return sx_response(result)
endpoint = f"sx_route_{name}"
view_fn = _route_view
if not _hdef.csrf:
view_fn = csrf_exempt(view_fn)
method = _hdef.method.lower()
route_reg = getattr(app_or_bp, method, None)
if route_reg is None:
logger.warning("Unsupported HTTP method %s for handler %s",
_hdef.method, name)
continue
route_reg(_hdef.path, endpoint=endpoint)(view_fn)
logger.info("Registered route %s %s → handler:%s",
_hdef.method.upper(), _hdef.path, name)
count += 1
return count
# ---------------------------------------------------------------------------
# Direct app mount — replaces per-service fragment blueprint boilerplate
# ---------------------------------------------------------------------------

View File

@@ -297,6 +297,81 @@ async def _io_g(
return getattr(g, key, None)
@register_io_handler("now")
async def _io_now(
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
) -> str:
"""``(now)`` or ``(now "%H:%M:%S")`` → formatted timestamp string."""
from datetime import datetime
fmt = str(args[0]) if args else None
dt = datetime.now()
return dt.strftime(fmt) if fmt else dt.isoformat()
@register_io_handler("sleep")
async def _io_sleep(
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
) -> Any:
"""``(sleep 800)`` → pause for 800ms."""
import asyncio
from .types import NIL
if not args:
raise ValueError("sleep requires milliseconds")
ms = int(args[0])
await asyncio.sleep(ms / 1000.0)
return NIL
@register_io_handler("request-form")
async def _io_request_form(
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
) -> Any:
"""``(request-form "name" default?)`` → read a form field."""
if not args:
raise ValueError("request-form requires a field name")
from quart import request
from .types import NIL
name = str(args[0])
default = args[1] if len(args) > 1 else NIL
form = await request.form
return form.get(name, default)
@register_io_handler("request-json")
async def _io_request_json(
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
) -> Any:
"""``(request-json)`` → JSON body as dict, or nil."""
from quart import request
from .types import NIL
data = await request.get_json(silent=True)
return data if data is not None else NIL
@register_io_handler("request-header")
async def _io_request_header(
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
) -> Any:
"""``(request-header "name" default?)`` → request header value."""
if not args:
raise ValueError("request-header requires a header name")
from quart import request
from .types import NIL
name = str(args[0])
default = args[1] if len(args) > 1 else NIL
return request.headers.get(name, default)
@register_io_handler("request-content-type")
async def _io_request_content_type(
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
) -> Any:
"""``(request-content-type)`` → content-type string or nil."""
from quart import request
from .types import NIL
return request.content_type or NIL
@register_io_handler("csrf-token")
async def _io_csrf_token(
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext

View File

@@ -138,6 +138,57 @@
"list" "dict" "sx-source"))
;; --------------------------------------------------------------------------
;; Web interop — reading non-SX request formats
;;
;; SX's native wire format is SX (text/sx). These primitives bridge to
;; legacy web formats: HTML form encoding, JSON bodies, HTTP headers.
;; They're useful for interop but not fundamental to SX-to-SX communication.
;; --------------------------------------------------------------------------
(define-io-primitive "now"
:params (&rest format)
:returns "string"
:async true
:doc "Current timestamp. Optional format string (strftime). Default ISO 8601."
:context :request)
(define-io-primitive "sleep"
:params (ms)
:returns "nil"
:async true
:doc "Pause execution for ms milliseconds. For demos and testing."
:context :request)
(define-io-primitive "request-form"
:params (name &rest default)
:returns "any"
:async true
:doc "Read a form field from a POST/PUT/PATCH request body."
:context :request)
(define-io-primitive "request-json"
:params ()
:returns "dict?"
:async true
:doc "Read JSON body from the current request, or nil if not JSON."
:context :request)
(define-io-primitive "request-header"
:params (name &rest default)
:returns "string?"
:async true
:doc "Read a request header value by name."
:context :request)
(define-io-primitive "request-content-type"
:params ()
:returns "string?"
:async true
:doc "Content-Type of the current request."
:context :request)
;; --------------------------------------------------------------------------
;; Tier 3: Signal primitives — reactive state for islands
;;

View File

@@ -38,17 +38,65 @@
;; --------------------------------------------------------------------------
;; defhandler — (defhandler name (&key param...) body)
;; defhandler — (defhandler name [:path "..." :method :get :csrf false] (&key param...) body)
;;
;; Keyword options between name and params list:
;; :path — public route path (string). Without :path, handler is internal-only.
;; :method — HTTP method (keyword: :get :post :put :patch :delete). Default :get.
;; :csrf — CSRF protection (boolean). Default true; set false for POST/PUT etc.
;; --------------------------------------------------------------------------
(define parse-handler-args
(fn ((args :as list))
"Parse defhandler args after the name symbol.
Scans for :keyword value option pairs, then a list (params), then body.
Returns dict with keys: opts, params, body."
(let ((opts {})
(params (list))
(body nil)
(i 0)
(n (len args))
(done false))
(for-each
(fn (idx)
(when (and (not done) (= idx i))
(let ((arg (nth args idx)))
(cond
;; keyword-value pair → consume two items
(= (type-of arg) "keyword")
(do
(when (< (+ idx 1) n)
(let ((val (nth args (+ idx 1))))
;; For :method, extract keyword name; for :csrf, keep as-is
(dict-set! opts (keyword-name arg)
(if (= (type-of val) "keyword")
(keyword-name val)
val))))
(set! i (+ idx 2)))
;; list → params, next element is body
(= (type-of arg) "list")
(do
(set! params (parse-key-params arg))
(when (< (+ idx 1) n)
(set! body (nth args (+ idx 1))))
(set! done true))
;; anything else → no explicit params, this is body
:else
(do
(set! body arg)
(set! done true))))))
(range 0 n))
(dict :opts opts :params params :body body))))
(define sf-defhandler
(fn ((args :as list) (env :as dict))
(let ((name-sym (first args))
(params-raw (nth args 1))
(body (nth args 2))
(name (symbol-name name-sym))
(params (parse-key-params params-raw)))
(let ((hdef (make-handler-def name params body env)))
(let ((name-sym (first args))
(name (symbol-name name-sym))
(parsed (parse-handler-args (rest args)))
(opts (get parsed "opts"))
(params (get parsed "params"))
(body (get parsed "body")))
(let ((hdef (make-handler-def name params body env opts)))
(env-set! env (str "handler:" name) hdef)
hdef))))

View File

@@ -222,8 +222,14 @@ def make_macro(params, rest_param, body, env, name=None):
closure=dict(env), name=name)
def make_handler_def(name, params, body, env):
return HandlerDef(name=name, params=list(params), body=body, closure=dict(env))
def make_handler_def(name, params, body, env, opts=None):
path = opts.get('path') if opts else None
method = str(opts.get('method', 'get')) if opts else 'get'
csrf = opts.get('csrf', True) if opts else True
if isinstance(csrf, str):
csrf = csrf.lower() not in ('false', 'nil', 'no')
return HandlerDef(name=name, params=list(params), body=body, closure=dict(env),
path=path, method=method.lower(), csrf=csrf)
def make_query_def(name, params, doc, body, env):

View File

@@ -181,8 +181,14 @@ def make_macro(params, rest_param, body, env, name=None):
closure=dict(env), name=name)
def make_handler_def(name, params, body, env):
return HandlerDef(name=name, params=list(params), body=body, closure=dict(env))
def make_handler_def(name, params, body, env, opts=None):
path = opts.get('path') if opts else None
method = str(opts.get('method', 'get')) if opts else 'get'
csrf = opts.get('csrf', True) if opts else True
if isinstance(csrf, str):
csrf = csrf.lower() not in ('false', 'nil', 'no')
return HandlerDef(name=name, params=list(params), body=body, closure=dict(env),
path=path, method=method.lower(), csrf=csrf)
def make_query_def(name, params, doc, body, env):
@@ -1855,14 +1861,43 @@ def parse_key_params(params_expr):
params.append(name)
return params
# parse-handler-args
def parse_handler_args(args):
_cells = {}
'Parse defhandler args after the name symbol.\n Scans for :keyword value option pairs, then a list (params), then body.\n Returns dict with keys: opts, params, body.'
opts = {}
_cells['params'] = []
_cells['body'] = NIL
_cells['i'] = 0
n = len(args)
_cells['done'] = False
for idx in range(0, n):
if sx_truthy(((not sx_truthy(_cells['done'])) if not sx_truthy((not sx_truthy(_cells['done']))) else (idx == _cells['i']))):
arg = nth(args, idx)
if sx_truthy((type_of(arg) == 'keyword')):
if sx_truthy(((idx + 1) < n)):
val = nth(args, (idx + 1))
opts[keyword_name(arg)] = (keyword_name(val) if sx_truthy((type_of(val) == 'keyword')) else val)
_cells['i'] = (idx + 2)
elif sx_truthy((type_of(arg) == 'list')):
_cells['params'] = parse_key_params(arg)
if sx_truthy(((idx + 1) < n)):
_cells['body'] = nth(args, (idx + 1))
_cells['done'] = True
else:
_cells['body'] = arg
_cells['done'] = True
return {'opts': opts, 'params': _cells['params'], 'body': _cells['body']}
# sf-defhandler
def sf_defhandler(args, env):
name_sym = first(args)
params_raw = nth(args, 1)
body = nth(args, 2)
name = symbol_name(name_sym)
params = parse_key_params(params_raw)
hdef = make_handler_def(name, params, body, env)
parsed = parse_handler_args(rest(args))
opts = get(parsed, 'opts')
params = get(parsed, 'params')
body = get(parsed, 'body')
hdef = make_handler_def(name, params, body, env, opts)
env[sx_str('handler:', name)] = hdef
return hdef

View File

@@ -221,18 +221,29 @@ class Island:
@dataclass
class HandlerDef:
"""A declarative fragment handler defined in an .sx file.
"""A declarative handler defined in an .sx file.
Created by ``(defhandler name (&key param...) body)``.
The body is evaluated in a sandboxed environment with only
s-expression primitives available.
Created by ``(defhandler name :path "/..." :method :get (&key param...) body)``.
When ``path`` is set, the handler is registered as a public route.
When ``path`` is None, it's an internal fragment handler (legacy behaviour).
"""
name: str
params: list[str] # keyword parameter names
body: Any # unevaluated s-expression body
closure: dict[str, Any] = field(default_factory=dict)
path: str | None = None # public route path (None = internal fragment only)
method: str = "get" # HTTP method (get, post, put, patch, delete)
csrf: bool = True # CSRF protection enabled
@property
def is_route(self) -> bool:
"""True if this handler has a public route path."""
return self.path is not None
def __repr__(self):
if self.path:
return f"<handler:{self.name} {self.method.upper()} {self.path}>"
return f"<handler:{self.name}({', '.join(self.params)})>"

View File

@@ -103,6 +103,14 @@ def create_app() -> "Quart":
bp = register_pages(url_prefix="/")
app.register_blueprint(bp)
# Register SX-defined route handlers (defhandler with :path)
from shared.sx.handlers import register_route_handlers
n_routes = register_route_handlers(app, "sx")
if n_routes:
import logging
logging.getLogger("sx.handlers").info(
"Registered %d route handler(s) for sx", n_routes)
from shared.sx.pages import auto_mount_pages
auto_mount_pages(app, "sx")

View File

@@ -710,7 +710,15 @@ def register(url_prefix: str = "/") -> Blueprint:
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
# ------------------------------------------------------------------
# Reference attribute detail API endpoints (for live demos)
# Reference API endpoints — remaining Python-only
#
# Most reference endpoints migrated to sx/sx/handlers/ref-api.sx.
# These remain because they need Python-specific features:
# - File upload access (request.files)
# - Dynamic all-params iteration
# - Stateful counters with non-200 responses
# - SSE streaming
# - Custom response headers
# ------------------------------------------------------------------
def _ref_wire(wire_id: str, sx_src: str) -> str:
@@ -718,105 +726,6 @@ def register(url_prefix: str = "/") -> Blueprint:
from sxc.pages.renders import _oob_code
return _oob_code(f"ref-wire-{wire_id}", sx_src)
@bp.get("/geography/hypermedia/reference/api/time")
async def ref_time():
from shared.sx.helpers import sx_response
now = datetime.now().strftime("%H:%M:%S")
sx_src = f'(span :class "text-stone-800 text-sm" "Server time: " (strong "{now}"))'
oob = _ref_wire("sx-get", sx_src)
return sx_response(f'(<> {sx_src} {oob})')
@csrf_exempt
@bp.post("/geography/hypermedia/reference/api/greet")
async def ref_greet():
from shared.sx.helpers import sx_response
form = await request.form
name = form.get("name") or "stranger"
sx_src = f'(span :class "text-stone-800 text-sm" "Hello, " (strong "{name}") "!")'
oob = _ref_wire("sx-post", sx_src)
return sx_response(f'(<> {sx_src} {oob})')
@csrf_exempt
@bp.put("/geography/hypermedia/reference/api/status")
async def ref_status():
from shared.sx.helpers import sx_response
form = await request.form
status = form.get("status", "unknown")
sx_src = f'(span :class "text-stone-700 text-sm" "Status: " (strong "{status}") " — updated via PUT")'
oob = _ref_wire("sx-put", sx_src)
return sx_response(f'(<> {sx_src} {oob})')
@csrf_exempt
@bp.patch("/geography/hypermedia/reference/api/theme")
async def ref_theme():
from shared.sx.helpers import sx_response
form = await request.form
theme = form.get("theme", "unknown")
sx_src = f'"{theme}"'
oob = _ref_wire("sx-patch", sx_src)
return sx_response(f'(<> {sx_src} {oob})')
@csrf_exempt
@bp.delete("/geography/hypermedia/reference/api/item/<item_id>")
async def ref_delete(item_id: str):
from shared.sx.helpers import sx_response
oob = _ref_wire("sx-delete", '""')
return sx_response(f'(<> {oob})')
@bp.get("/geography/hypermedia/reference/api/trigger-search")
async def ref_trigger_search():
from shared.sx.helpers import sx_response
q = request.args.get("q", "")
if not q:
sx_src = '(span :class "text-stone-400 text-sm" "Start typing to trigger a search.")'
else:
sx_src = f'(span :class "text-stone-800 text-sm" "Results for: " (strong "{q}"))'
oob = _ref_wire("sx-trigger", sx_src)
return sx_response(f'(<> {sx_src} {oob})')
@bp.get("/geography/hypermedia/reference/api/swap-item")
async def ref_swap_item():
from shared.sx.helpers import sx_response
now = datetime.now().strftime("%H:%M:%S")
sx_src = f'(div :class "text-sm text-violet-700" "New item (" "{now}" ")")'
oob = _ref_wire("sx-swap", sx_src)
return sx_response(f'(<> {sx_src} {oob})')
@bp.get("/geography/hypermedia/reference/api/oob")
async def ref_oob():
from shared.sx.helpers import sx_response
now = datetime.now().strftime("%H:%M:%S")
sx_src = (
f'(<>'
f' (span :class "text-emerald-700 text-sm" "Main updated at " "{now}")'
f' (div :id "ref-oob-side" :sx-swap-oob "innerHTML"'
f' (span :class "text-violet-700 text-sm" "OOB updated at " "{now}")))')
oob = _ref_wire("sx-swap-oob", sx_src)
return sx_response(f'(<> {sx_src} {oob})')
@bp.get("/geography/hypermedia/reference/api/select-page")
async def ref_select_page():
from shared.sx.helpers import sx_response
now = datetime.now().strftime("%H:%M:%S")
sx_src = (
f'(<>'
f' (div :id "the-header" (h3 "Page header — not selected"))'
f' (div :id "the-content"'
f' (span :class "text-emerald-700 text-sm"'
f' "This fragment was selected from a larger response. Time: " "{now}"))'
f' (div :id "the-footer" (p "Page footer — not selected")))')
oob = _ref_wire("sx-select", sx_src)
return sx_response(f'(<> {sx_src} {oob})')
@bp.get("/geography/hypermedia/reference/api/slow-echo")
async def ref_slow_echo():
from shared.sx.helpers import sx_response
await asyncio.sleep(0.8)
q = request.args.get("q", "")
sx_src = f'(span :class "text-stone-800 text-sm" "Echo: " (strong "{q}"))'
oob = _ref_wire("sx-sync", sx_src)
return sx_response(f'(<> {sx_src} {oob})')
@csrf_exempt
@bp.post("/geography/hypermedia/reference/api/upload-name")
async def ref_upload_name():
@@ -882,14 +791,6 @@ def register(url_prefix: str = "/") -> Blueprint:
oob = _ref_wire("sx-retry", sx_src)
return sx_response(f'(<> {sx_src} {oob})')
@bp.get("/geography/hypermedia/reference/api/prompt-echo")
async def ref_prompt_echo():
from shared.sx.helpers import sx_response
name = request.headers.get("SX-Prompt", "anonymous")
sx_src = f'(span :class "text-stone-800 text-sm" "Hello, " (strong "{name}") "!")'
oob = _ref_wire("sx-prompt", sx_src)
return sx_response(f'(<> {sx_src} {oob})')
@bp.get("/geography/hypermedia/reference/api/sse-time")
async def ref_sse_time():
async def generate():
@@ -1049,10 +950,4 @@ def register(url_prefix: str = "/") -> Blueprint:
resp.headers["SX-Retarget"] = "#ref-hdr-retarget-alt"
return resp
# --- Event demos ---
@bp.get("/geography/hypermedia/reference/api/error-500")
async def ref_error_500():
return Response("Server error", status=500, content_type="text/plain")
return bp

View File

@@ -0,0 +1,73 @@
;; ---------------------------------------------------------------------------
;; The Art Chain
;; ---------------------------------------------------------------------------
(defcomp ~essay-the-art-chain ()
(~doc-page :title "The Art Chain"
(p :class "text-stone-500 text-sm italic mb-8"
"On making, self-making, and the chain of artifacts that produces itself.")
(~doc-section :title "I. Ars" :id "ars"
(p :class "text-stone-600"
"The Latin word " (em "ars") " means something made with skill. Not art as in paintings on gallery walls. Art as in " (em "artifice") ", " (em "artifact") ", " (em "artisan") ". The made thing. The Greek " (em "techne") " is the same word — craft, skill, the knowledge of how to make. There was no distinction between art and engineering because there was no distinction to make.")
(p :class "text-stone-600"
"A bridge is " (em "ars") ". A poem is " (em "ars") ". A proof is " (em "ars") ". What makes something art is not its medium or its audience but the fact that it was " (em "made") " — brought into being by someone who knew how to bring it into being. The maker's knowledge is embedded in the made thing. You can read the knowledge back out by studying what was made.")
(p :class "text-stone-600"
"Software is " (em "ars") ". Obviously. It is the most " (em "ars") " thing we have ever built — pure made-ness, structure conjured from nothing, shaped entirely by the maker's skill and intent. There is no raw material. No marble to chisel, no pigment to mix. Just thought, made concrete in symbols."))
(~doc-section :title "II. The spec at the centre" :id "spec"
(p :class "text-stone-600"
"SX has a peculiar architecture. At its centre sits a specification — a set of s-expression files that define the language. Not a description of the language. Not documentation " (em "about") " the language. The specification " (em "is") " the language. It is simultaneously a formal definition and executable code. You can read it as a document or run it as a program. It does not describe how to build an SX evaluator; it " (em "is") " an SX evaluator, expressed in the language it defines.")
(p :class "text-stone-600"
"This is the nucleus. Everything else radiates outward from it.")
(~doc-code :code (highlight ";; The spec defines eval-expr\n;; eval-expr evaluates the spec\n;; The spec is an artifact that makes itself\n\n(define eval-expr\n (fn (expr env)\n (cond\n (number? expr) expr\n (string? expr) expr\n (symbol? expr) (env-get env (symbol-name expr))\n (list? expr) (eval-list expr env)\n :else expr)))" "lisp"))
(p :class "text-stone-600"
"From this nucleus, concentric rings unfurl:"))
(~doc-section :title "III. The rings" :id "rings"
(p :class "text-stone-600"
"The first ring is the " (strong "bootstrapper") ". It reads the spec and emits a native implementation — JavaScript, Python, or any other target. The bootstrapper is a translator: it takes the made thing (the spec) and makes another thing (an implementation) that behaves identically. The spec's knowledge is preserved in the translation. Nothing is added, nothing is lost.")
(p :class "text-stone-600"
"The second ring is the " (strong "platform bridge") ". The spec defines pure logic — evaluation, rendering, parsing. But a running system needs to touch the world: read files, make HTTP requests, manipulate DOM nodes. The platform bridge provides these capabilities. It is the boundary between the made world (the spec) and the found world (the host environment). " (code "boundary.sx") " is literally the membrane — it declares what the host must provide so the spec can function.")
(p :class "text-stone-600"
"The third ring is the " (strong "runtime") " — bootstrapped spec plus platform bridge, assembled into a working system. This is where the spec stops being an idea and starts being a process. It evaluates expressions. It renders pages. It handles requests.")
(p :class "text-stone-600"
"The fourth ring is " (strong "application code") " — components, pages, layouts, written in the language the spec defined. Every " (code "defcomp") " is an artifact made from the tools the spec provided. Every " (code "(div :class \"card\" (p \"hello\"))") " is the spec expressing itself through a developer's intent.")
(p :class "text-stone-600"
"The fifth ring is " (strong "this website") " — which renders the spec's source code using the runtime the spec produced, displayed in components written in the language the spec defines, navigated by an engine the spec specifies. The documentation is the thing documenting itself."))
(~doc-section :title "IV. The chain" :id "chain"
(p :class "text-stone-600"
"Each ring is an artifact — a made thing. And each artifact is made " (em "by") " the artifact inside it. The spec makes the bootstrapper's output. The runtime makes the application's output. The application makes the page the user sees. It is a chain of making.")
(p :class "text-stone-600"
"This chain has three properties that are individually common but collectively rare:")
(p :class "text-stone-600"
(strong "Content addressing.") " Each artifact can be identified by the hash of its content. The spec at a given version has a specific hash. The bootstrapped output from that spec has a deterministic hash. A component definition has a hash. Identity " (em "is") " content. You don't ask " (em "where") " an artifact lives — you ask " (em "what") " it is.")
(p :class "text-stone-600"
(strong "Deterministic derivation.") " Given the same spec, the bootstrapper produces the same output. Byte for byte. This is not aspirational — it is verified. The self-hosting bootstrapper (py.sx) proves it: G0 (hand-written bootstrapper) and G1 (self-hosted bootstrapper) produce identical output. The derivation is a pure function. Anyone can run it and verify the result.")
(p :class "text-stone-600"
(strong "Self-verification.") " The spec includes tools that can prove properties about the spec. " (code "prove.sx") " checks primitive semantics. " (code "types.sx") " validates composition. " (code "z3.sx") " translates declarations into verification conditions. These tools are themselves part of the spec, subject to the same verification they perform. The chain can verify itself.")
(p :class "text-stone-600"
"These three properties together — content addressing, deterministic derivation, self-verification — are what a blockchain provides. But here there is no proof-of-work, no tokens, no artificial scarcity, no consensus mechanism between untrusted parties. The \"mining\" is bootstrapping. The \"consensus\" is mathematical proof. The \"value\" is that anyone can take the spec, derive an implementation, and " (em "know") " it is correct."))
(~doc-section :title "V. Universal analysis" :id "analysis"
(p :class "text-stone-600"
"Here is the consequence that takes time to absorb: any tool that can analyse the spec can analyse " (em "everything the spec produces") ".")
(p :class "text-stone-600"
"A type checker written in SX that validates the spec's primitives also validates every call to those primitives in every component in every application. A dependency analyser that walks the spec's AST walks application ASTs identically — because application code is expressed in the same structures the spec defines. A theorem prover that verifies the spec's properties verifies the properties of everything downstream.")
(p :class "text-stone-600"
"This is because the rings are not separate systems. They are the " (em "same") " system at different scales. Application code is spec-shaped. Bootstrapped output is spec-derived. Components are spec-evaluated. The analysis surface is uniform from the nucleus to the outermost ring.")
(p :class "text-stone-600"
"And the analysis tools are " (em "inside") " the chain. They are artifacts too, written in SX, subject to the same analysis they perform. The type checker can type-check itself. The prover can prove properties about itself. This is not a bug or a curiosity — it is the point. A system that cannot reason about itself is a system that must be reasoned about from outside, by tools written in other languages, maintained by other processes, trusted for other reasons. A self-analysing system closes the loop."))
(~doc-section :title "VI. The art in the chain" :id "art"
(p :class "text-stone-600"
"So what is the art chain? It is a chain of artifacts — made things — where each link produces the next, the whole chain can verify itself, and the chain's identity is its content.")
(p :class "text-stone-600"
"It is not a blockchain in the financial sense. It is not a distributed ledger, a currency, a market. It borrows the structural properties — content addressing, determinism, verification — without the economic machinery. What remains when you strip the economics from a blockchain is a " (em "provenance chain") ": a record of how each thing was made from the thing before it, verifiable by anyone, depending on nothing but the mathematics.")
(p :class "text-stone-600"
"The Art DAG has the right name. It is not a system for processing \"art\" in the colloquial sense — images, videos, media. It is a " (em "directed acyclic graph of made things") ". Each node is an artifact. Each edge is a derivation. The graph is content-addressed. Execution is deterministic. The DAG itself is the art.")
(p :class "text-stone-600"
"And the whole SX system — spec, bootstrappers, runtimes, components, pages, this essay explaining itself — is one continuous act of making. " (em "Ars") " all the way down. Not because it is beautiful (though it sometimes is) or expressive (though it tries to be) but because it is " (em "made") ". Deliberately, skilfully, from nothing, by someone who knew how.")
(p :class "text-stone-600"
"That is what " (em "techne") " always was. We just forgot."))))

156
sx/sx/handlers/ref-api.sx Normal file
View File

@@ -0,0 +1,156 @@
;; Reference API endpoints — live demos for hypermedia attribute docs
;;
;; These replace the Python endpoints in bp/pages/routes.py.
;; Each defhandler with :path registers as a public route automatically.
;; --- sx-get demo: server time ---
(defhandler ref-time
:path "/geography/hypermedia/reference/api/time"
:method :get
(&key)
(let ((now (now "%H:%M:%S")))
(<>
(span :class "text-stone-800 text-sm" "Server time: " (strong now))
(~doc-oob-code :target-id "ref-wire-sx-get"
:text (str "(span :class \"text-stone-800 text-sm\" \"Server time: \" (strong \"" now "\"))")))))
;; --- sx-post demo: greet ---
(defhandler ref-greet
:path "/geography/hypermedia/reference/api/greet"
:method :post
:csrf false
(&key)
(let ((name (request-form "name" "stranger")))
(<>
(span :class "text-stone-800 text-sm" "Hello, " (strong name) "!")
(~doc-oob-code :target-id "ref-wire-sx-post"
:text (str "(span :class \"text-stone-800 text-sm\" \"Hello, \" (strong \"" name "\") \"!\")")))))
;; --- sx-put demo: status update ---
(defhandler ref-status
:path "/geography/hypermedia/reference/api/status"
:method :put
:csrf false
(&key)
(let ((status (request-form "status" "unknown")))
(<>
(span :class "text-stone-700 text-sm" "Status: " (strong status) " — updated via PUT")
(~doc-oob-code :target-id "ref-wire-sx-put"
:text (str "(span :class \"text-stone-700 text-sm\" \"Status: \" (strong \"" status "\") \" — updated via PUT\")")))))
;; --- sx-patch demo: theme ---
(defhandler ref-theme
:path "/geography/hypermedia/reference/api/theme"
:method :patch
:csrf false
(&key)
(let ((theme (request-form "theme" "unknown")))
(<>
theme
(~doc-oob-code :target-id "ref-wire-sx-patch"
:text (str "\"" theme "\"")))))
;; --- sx-delete demo ---
(defhandler ref-delete-item
:path "/geography/hypermedia/reference/api/item/<item_id>"
:method :delete
:csrf false
(&key)
(<>
(~doc-oob-code :target-id "ref-wire-sx-delete" :text "\"\"")))
;; --- sx-trigger demo: search ---
(defhandler ref-trigger-search
:path "/geography/hypermedia/reference/api/trigger-search"
:method :get
(&key)
(let ((q (request-arg "q" "")))
(let ((sx-text (if (= q "")
"(span :class \"text-stone-400 text-sm\" \"Start typing to trigger a search.\")"
(str "(span :class \"text-stone-800 text-sm\" \"Results for: \" (strong \"" q "\"))"))))
(<>
(if (= q "")
(span :class "text-stone-400 text-sm" "Start typing to trigger a search.")
(span :class "text-stone-800 text-sm" "Results for: " (strong q)))
(~doc-oob-code :target-id "ref-wire-sx-trigger" :text sx-text)))))
;; --- sx-swap demo ---
(defhandler ref-swap-item
:path "/geography/hypermedia/reference/api/swap-item"
:method :get
(&key)
(let ((now (now "%H:%M:%S")))
(<>
(div :class "text-sm text-violet-700" (str "New item (" now ")"))
(~doc-oob-code :target-id "ref-wire-sx-swap"
:text (str "(div :class \"text-sm text-violet-700\" \"New item (" now ")\")")))))
;; --- sx-swap-oob demo ---
(defhandler ref-oob
:path "/geography/hypermedia/reference/api/oob"
:method :get
(&key)
(let ((now (now "%H:%M:%S")))
(<>
(span :class "text-emerald-700 text-sm" "Main updated at " now)
(div :id "ref-oob-side" :sx-swap-oob "innerHTML"
(span :class "text-violet-700 text-sm" "OOB updated at " now))
(~doc-oob-code :target-id "ref-wire-sx-swap-oob"
:text (str "(<> (span ... \"" now "\") (div :id \"ref-oob-side\" :sx-swap-oob \"innerHTML\" ...))")))))
;; --- sx-select demo ---
(defhandler ref-select-page
:path "/geography/hypermedia/reference/api/select-page"
:method :get
(&key)
(let ((now (now "%H:%M:%S")))
(<>
(div :id "the-header" (h3 "Page header — not selected"))
(div :id "the-content"
(span :class "text-emerald-700 text-sm"
"This fragment was selected from a larger response. Time: " now))
(div :id "the-footer" (p "Page footer — not selected"))
(~doc-oob-code :target-id "ref-wire-sx-select"
:text (str "(<> (div :id \"the-header\" ...) (div :id \"the-content\" ... \"" now "\") (div :id \"the-footer\" ...))")))))
;; --- sx-sync demo: slow echo ---
(defhandler ref-slow-echo
:path "/geography/hypermedia/reference/api/slow-echo"
:method :get
(&key)
(let ((q (request-arg "q" "")))
(sleep 800)
(<>
(span :class "text-stone-800 text-sm" "Echo: " (strong q))
(~doc-oob-code :target-id "ref-wire-sx-sync"
:text (str "(span :class \"text-stone-800 text-sm\" \"Echo: \" (strong \"" q "\"))")))))
;; --- sx-prompt demo ---
(defhandler ref-prompt-echo
:path "/geography/hypermedia/reference/api/prompt-echo"
:method :get
(&key)
(let ((name (request-header "SX-Prompt" "anonymous")))
(<>
(span :class "text-stone-800 text-sm" "Hello, " (strong name) "!")
(~doc-oob-code :target-id "ref-wire-sx-prompt"
:text (str "(span :class \"text-stone-800 text-sm\" \"Hello, \" (strong \"" name "\") \"!\")")))))
;; --- Error demo ---
(defhandler ref-error-500
:path "/geography/hypermedia/reference/api/error-500"
:method :get
(&key)
(abort 500 "Server error"))

View File

@@ -95,7 +95,9 @@
(dict :label "React is Hypermedia" :href "/etc/essays/react-is-hypermedia"
:summary "A React Island is a hypermedia control. Its behavior is specified in SX.")
(dict :label "The Hegelian Synthesis" :href "/etc/essays/hegelian-synthesis"
:summary "On the dialectical resolution of the hypertext/reactive contradiction. Thesis: the server renders. Antithesis: the client reacts. Synthesis: the island in the lake.")))
:summary "On the dialectical resolution of the hypertext/reactive contradiction. Thesis: the server renders. Antithesis: the client reacts. Synthesis: the island in the lake.")
(dict :label "The Art Chain" :href "/etc/essays/the-art-chain"
:summary "On making, self-making, and the chain of artifacts that produces itself. Ars, techne, content addressing, and why the spec is the art.")))
(define philosophy-nav-items (list
(dict :label "The SX Manifesto" :href "/etc/philosophy/sx-manifesto"
@@ -110,25 +112,33 @@
:summary "Existence precedes essence — Sartre, Camus, and the absurd freedom of writing a Lisp for the web.")))
(define specs-nav-items (list
(dict :label "Architecture" :href "/language/specs/")
(dict :label "Core" :href "/language/specs/core")
(dict :label "Parser" :href "/language/specs/parser")
(dict :label "Evaluator" :href "/language/specs/evaluator")
(dict :label "Primitives" :href "/language/specs/primitives")
(dict :label "Special Forms" :href "/language/specs/special-forms")
(dict :label "Renderer" :href "/language/specs/renderer")
(dict :label "Adapters" :href "/language/specs/adapters")
(dict :label "DOM Adapter" :href "/language/specs/adapter-dom")
(dict :label "HTML Adapter" :href "/language/specs/adapter-html")
(dict :label "SX Wire Adapter" :href "/language/specs/adapter-sx")
(dict :label "Browser" :href "/language/specs/browser")
(dict :label "SxEngine" :href "/language/specs/engine")
(dict :label "Orchestration" :href "/language/specs/orchestration")
(dict :label "Boot" :href "/language/specs/boot")
(dict :label "Continuations" :href "/language/specs/continuations")
(dict :label "call/cc" :href "/language/specs/callcc")
(dict :label "Deps" :href "/language/specs/deps")
(dict :label "Router" :href "/language/specs/router")))
{:label "Core" :href "/language/specs/core" :children (list
{:label "Parser" :href "/language/specs/parser"}
{:label "Evaluator" :href "/language/specs/evaluator"}
{:label "Primitives" :href "/language/specs/primitives"}
{:label "Special Forms" :href "/language/specs/special-forms"}
{:label "Renderer" :href "/language/specs/renderer"})}
{:label "Adapters" :href "/language/specs/adapters" :children (list
{:label "DOM Adapter" :href "/language/specs/adapter-dom"}
{:label "HTML Adapter" :href "/language/specs/adapter-html"}
{:label "SX Wire Adapter" :href "/language/specs/adapter-sx"}
{:label "Async Adapter" :href "/language/specs/adapter-async"})}
{:label "Browser" :href "/language/specs/browser" :children (list
{:label "SxEngine" :href "/language/specs/engine"}
{:label "Orchestration" :href "/language/specs/orchestration"}
{:label "Boot" :href "/language/specs/boot"}
{:label "Router" :href "/language/specs/router"})}
{:label "Reactive" :href "/language/specs/reactive" :children (list
{:label "Signals" :href "/language/specs/signals"})}
{:label "Host Interface" :href "/language/specs/host" :children (list
{:label "Boundary" :href "/language/specs/boundary"}
{:label "Forms" :href "/language/specs/forms"}
{:label "Page Helpers" :href "/language/specs/page-helpers"})}
{:label "Extensions" :href "/language/specs/extensions" :children (list
{:label "Continuations" :href "/language/specs/continuations"}
{:label "call/cc" :href "/language/specs/callcc"}
{:label "Types" :href "/language/specs/types"}
{:label "Deps" :href "/language/specs/deps"})}))
(define testing-nav-items (list
(dict :label "Overview" :href "/language/testing/")
@@ -260,17 +270,39 @@
(dict :slug "adapter-sx" :filename "adapter-sx.sx" :title "SX Wire Adapter"
:desc "Serializes SX for client-side rendering. Component calls stay unexpanded."
:prose "The SX wire adapter serializes expressions as SX source text for transmission to the browser, where sx.js renders them client-side. Unlike the HTML adapter, component calls (~name ...) are NOT expanded — they are sent to the client as-is, allowing the browser to render them with its local component registry. HTML tags ARE serialized as s-expression source. This is the format used for SX-over-HTTP responses and the page boot payload.")
(dict :slug "adapter-async" :filename "adapter-async.sx" :title "Async Adapter"
:desc "Async versions of HTML and SX wire adapters for server-side rendering with I/O."
:prose "The async adapter provides async-aware versions of the HTML and SX wire rendering functions. It intercepts I/O operations (database queries, service calls, fragment fetches) during evaluation, awaiting them before continuing. Entry points: async-render (HTML output with awaited I/O), async-aser (SX wire format with awaited I/O). The bootstrapper emits async def and automatic await insertion for all define-async functions. This adapter is what makes server-side SX pages work with real data.")))
(define browser-spec-items (list
(dict :slug "engine" :filename "engine.sx" :title "SxEngine"
:desc "Pure logic for fetch, swap, history, SSE, triggers, morph, and indicators."
:prose "The engine specifies the pure logic of the browser-side fetch/swap/history system. Like HTMX but native to SX. It defines trigger parsing (click, submit, intersect, poll, load, revealed), swap algorithms (innerHTML, outerHTML, morph, beforebegin, etc.), the morph/diff algorithm for patching existing DOM, history management (push-url, replace-url, popstate), out-of-band swap identification, Server-Sent Events parsing, retry logic with exponential backoff, request header building, response header processing, and optimistic UI updates. This file contains no browser API calls — all platform interaction is in orchestration.sx.")
(dict :slug "orchestration" :filename "orchestration.sx" :title "Orchestration"
:desc "Browser wiring that binds engine logic to DOM events, fetch, and lifecycle."
:prose "Orchestration is the browser wiring layer. It binds the pure engine logic to actual browser APIs: DOM event listeners, fetch(), AbortController, setTimeout/setInterval, IntersectionObserver, history.pushState, and EventSource (SSE). It implements the full request lifecycle — from trigger through fetch through swap — including CSS tracking, response type detection (SX vs HTML), OOB swap processing, script activation, element boosting, and preload. Dependency is strictly one-way: orchestration depends on engine, never the reverse.")))
(define browser-spec-items (list
(dict :slug "boot" :filename "boot.sx" :title "Boot"
:prose "Orchestration is the browser wiring layer. It binds the pure engine logic to actual browser APIs: DOM event listeners, fetch(), AbortController, setTimeout/setInterval, IntersectionObserver, history.pushState, and EventSource (SSE). It implements the full request lifecycle — from trigger through fetch through swap — including CSS tracking, response type detection (SX vs HTML), OOB swap processing, script activation, element boosting, and preload. Dependency is strictly one-way: orchestration depends on engine, never the reverse.")
(dict :slug "boot" :filename "boot.sx" :title "Boot"
:desc "Browser startup lifecycle: mount, hydrate, script processing."
:prose "Boot handles the browser startup sequence and provides the public API for mounting SX content. On page load it: (1) initializes CSS tracking, (2) processes <script type=\"text/sx\"> tags (component definitions and mount directives), (3) hydrates [data-sx] elements, and (4) activates the engine on all elements. It also provides the public mount/hydrate/update/render-component API, and the head element hoisting logic that moves <meta>, <title>, and <link> tags from rendered content into <head>.")))
:prose "Boot handles the browser startup sequence and provides the public API for mounting SX content. On page load it: (1) initializes CSS tracking, (2) processes <script type=\"text/sx\"> tags (component definitions and mount directives), (3) hydrates [data-sx] elements, and (4) activates the engine on all elements. It also provides the public mount/hydrate/update/render-component API, and the head element hoisting logic that moves <meta>, <title>, and <link> tags from rendered content into <head>.")
(dict :slug "router" :filename "router.sx" :title "Router"
:desc "Client-side route matching — Flask-style pattern parsing, segment matching, route table search."
:prose "The router module provides pure functions for matching URL paths against Flask-style route patterns (e.g. /docs/<slug>). Used by client-side routing to determine if a page can be rendered locally without a server roundtrip. split-path-segments breaks a path into segments, parse-route-pattern converts patterns into typed segment descriptors, match-route-segments tests a path against a parsed pattern returning extracted params, and find-matching-route searches a route table for the first match.")))
(define reactive-spec-items (list
(dict :slug "signals" :filename "signals.sx" :title "Signals"
:desc "Fine-grained reactive primitives — signal, computed, effect, batch."
:prose "The signals module defines a fine-grained reactive system for client-side islands. Signals are containers for values that notify subscribers on change. Computed signals derive values lazily from other signals. Effects run side-effects when their dependencies change, with automatic cleanup. Batch coalesces multiple signal writes into a single notification pass. Island scope management ensures all signals, computeds, and effects are cleaned up when an island is removed from the DOM. The spec defines the reactive graph topology and update algorithm — each platform implements the actual signal/tracking types natively.")))
(define host-spec-items (list
(dict :slug "boundary" :filename "boundary.sx" :title "Boundary"
:desc "Language boundary contract — declares I/O primitives the host must provide."
:prose "The boundary defines the contract between SX and its host environment. Tier 1 declares pure primitives (from primitives.sx). Tier 2 declares async I/O primitives the host must implement: fetch, async-eval, call-action, send-activity, and other operations that require network or database access. Tier 3 declares page helpers: format, highlight, scan-css-classes, parse-datetime. This is the interface every host must satisfy to run SX — framework-agnostic, universal to all targets. Boundary enforcement validates at registration time that all declared primitives are provided.")
(dict :slug "forms" :filename "forms.sx" :title "Forms"
:desc "Server-side definition forms — defhandler, defquery, defaction, defpage."
:prose "Forms defines the server-side definition macros that compose the application layer. defhandler registers an HTTP route handler. defquery defines a read-only data source. defaction defines a mutation (write). defpage declares a client-routable page with path, auth, layout, data dependencies, and content. Each form parses &key parameter lists and creates typed definition objects. Platform-specific constructors are provided by the host — these have different bindings on server (Python/Quart) vs client (route matching only).")
(dict :slug "page-helpers" :filename "page-helpers.sx" :title "Page Helpers"
:desc "Pure data-transformation helpers for page rendering."
:prose "Page helpers are pure functions that assist page rendering: categorizing special forms by type, formatting numbers and dates, highlighting code, scanning CSS classes, constructing page titles and descriptions. Unlike boundary I/O primitives, these are pure — they take data and return data with no side effects. They run identically on server and client. The host registers native implementations that match these declarations.")))
(define extension-spec-items (list
(dict :slug "continuations" :filename "continuations.sx" :title "Continuations"
@@ -278,17 +310,15 @@
:prose "Delimited continuations capture the rest of a computation up to a delimiter. shift captures the continuation to the nearest reset as a first-class callable value. Unlike full call/cc, delimited continuations are composable — invoking one returns a value. This covers the practical use cases: suspendable server rendering, cooperative scheduling, linear async flows, wizard-style multi-step UIs, and undo. Each bootstrapper target implements the mechanism differently — generators in Python/JS, native shift/reset in Scheme, ContT in Haskell, CPS transform in Rust — but the semantics are identical. Optional extension: code that doesn't use continuations pays zero cost.")
(dict :slug "callcc" :filename "callcc.sx" :title "call/cc"
:desc "Full first-class continuations — call-with-current-continuation."
:prose "Full call/cc captures the entire remaining computation as a first-class function — not just up to a delimiter, but all the way to the top level. Invoking the continuation abandons the current computation entirely and resumes from where it was captured. Strictly more powerful than delimited continuations, but harder to implement in targets that don't support it natively. Recommended for Scheme and Haskell targets where it's natural. Python, JavaScript, and Rust targets should prefer delimited continuations (continuations.sx) unless full escape semantics are genuinely needed. Optional extension: the continuation type is shared with continuations.sx if both are loaded.")))
(define module-spec-items (list
:prose "Full call/cc captures the entire remaining computation as a first-class function — not just up to a delimiter, but all the way to the top level. Invoking the continuation abandons the current computation entirely and resumes from where it was captured. Strictly more powerful than delimited continuations, but harder to implement in targets that don't support it natively. Recommended for Scheme and Haskell targets where it's natural. Python, JavaScript, and Rust targets should prefer delimited continuations (continuations.sx) unless full escape semantics are genuinely needed. Optional extension: the continuation type is shared with continuations.sx if both are loaded.")
(dict :slug "types" :filename "types.sx" :title "Types"
:desc "Gradual type system — registration-time checking with zero runtime cost."
:prose "The types module defines a gradual type system for SX. Type annotations on function parameters and return values are checked at registration time (when defcomp or define is evaluated), not at every call site. Base types include number, string, boolean, nil, symbol, keyword, element, any, and never. Union types (string|nil), function types, and type narrowing through control flow are supported. The system catches composition errors and boundary mismatches at definition time without any runtime overhead — unannotated code is unaffected.")
(dict :slug "deps" :filename "deps.sx" :title "Deps"
:desc "Component dependency analysis and IO detection — per-page bundling, transitive closure, CSS scoping, pure/IO classification."
:prose "The deps module analyzes component dependency graphs and classifies components as pure or IO-dependent. Phase 1 (bundling): walks component AST bodies to find transitive ~component references, computes the minimal set needed per page, and collects per-page CSS classes from only the used components. Phase 2 (IO detection): scans component ASTs for references to IO primitive names (from boundary.sx declarations — frag, query, service, current-user, highlight, etc.), computes transitive IO refs through the component graph, and caches the result on each component. Components with no transitive IO refs are pure — they can render anywhere without server data. IO-dependent components must expand server-side. The spec provides the classification; each host's async partial evaluator acts on it (expand IO-dependent server-side, serialize pure for client). All functions are pure — each host bootstraps them to native code via --spec-modules deps. Platform functions (component-deps, component-set-deps!, component-css-classes, component-io-refs, component-set-io-refs!, env-components, regex-find-all, scan-css-classes) are implemented natively per target.")
(dict :slug "router" :filename "router.sx" :title "Router"
:desc "Client-side route matching — Flask-style pattern parsing, segment matching, route table search."
:prose "The router module provides pure functions for matching URL paths against Flask-style route patterns (e.g. /docs/<slug>). Used by client-side routing (Phase 3) to determine if a page can be rendered locally without a server roundtrip. split-path-segments breaks a path into segments, parse-route-pattern converts patterns into typed segment descriptors, match-route-segments tests a path against a parsed pattern returning extracted params, and find-matching-route searches a route table for the first match. No platform interface needed — uses only pure string and list primitives. Bootstrapped via --spec-modules deps,router.")))
:desc "Component dependency analysis and IO detection — per-page bundling, transitive closure, CSS scoping."
:prose "The deps module analyzes component dependency graphs and classifies components as pure or IO-dependent. Phase 1 (bundling): walks component AST bodies to find transitive ~component references, computes the minimal set needed per page, and collects per-page CSS classes from only the used components. Phase 2 (IO detection): scans component ASTs for references to IO primitive names (from boundary.sx declarations), computes transitive IO refs through the component graph, and caches the result. Components with no transitive IO refs are pure — they can render anywhere without server data. IO-dependent components must expand server-side.")))
(define all-spec-items (concat core-spec-items (concat adapter-spec-items (concat browser-spec-items (concat extension-spec-items module-spec-items)))))
(define all-spec-items (concat core-spec-items (concat adapter-spec-items (concat browser-spec-items (concat reactive-spec-items (concat host-spec-items extension-spec-items))))))
(define find-spec
(fn (slug)

View File

@@ -12,9 +12,13 @@
(p :class "text-lg text-stone-600"
"SX is defined in SX. The canonical specification is a set of s-expression files that are both documentation and executable definition. Bootstrap compilers read these files to generate native implementations in JavaScript, Python, Rust, or any other target.")
(p :class "text-stone-600"
"The spec is split into two layers: a "
(strong "core") " that defines the language itself, and "
(strong "adapters") " that connect it to specific environments."))
"The spec is organized into six sections: "
(strong "Core") " (the language itself), "
(strong "Adapters") " (rendering backends), "
(strong "Browser") " (client-side runtime), "
(strong "Reactive") " (signal system), "
(strong "Host Interface") " (platform contract), and "
(strong "Extensions") " (optional add-ons)."))
(div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Core")
@@ -96,18 +100,20 @@
:sx-swap "outerHTML" :sx-push-url "true"
"adapter-sx.sx"))
(td :class "px-3 py-2 text-stone-700" "SX wire format")
(td :class "px-3 py-2 text-stone-500" "Server to client"))))))
(td :class "px-3 py-2 text-stone-500" "Server to client"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700"
(a :href "/language/specs/adapter-async" :class "hover:underline"
:sx-get "/language/specs/adapter-async" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
"adapter-async.sx"))
(td :class "px-3 py-2 text-stone-700" "HTML/SX with awaited I/O")
(td :class "px-3 py-2 text-stone-500" "Server (async)"))))))
(div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Engine")
(h2 :class "text-2xl font-semibold text-stone-800" "Browser Runtime")
(p :class "text-stone-600"
"The engine is the browser-side fetch/swap/history system. It processes "
(code :class "text-violet-700 text-sm" "sx-*")
" attributes on elements to make HTTP requests, swap content, manage browser history, and handle events. It is split into two files: pure logic ("
(code :class "text-violet-700 text-sm" "engine.sx")
") and browser wiring ("
(code :class "text-violet-700 text-sm" "orchestration.sx")
").")
"The browser runtime handles the full client-side lifecycle: parsing triggers, making HTTP requests, swapping content, managing history, and booting the page. Split into pure logic (engine), browser wiring (orchestration), startup (boot), and URL matching (router).")
(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"
@@ -127,16 +133,26 @@
:sx-get "/language/specs/orchestration" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
"orchestration.sx"))
(td :class "px-3 py-2 text-stone-700" "Browser wiring — binds engine to DOM events, fetch, request lifecycle"))))))
(td :class "px-3 py-2 text-stone-700" "Browser wiring — binds engine to DOM events, fetch, request lifecycle"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700"
(a :href "/language/specs/boot" :class "hover:underline"
:sx-get "/language/specs/boot" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
"boot.sx"))
(td :class "px-3 py-2 text-stone-700" "Browser startup — mount, hydrate, script processing, head hoisting"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700"
(a :href "/language/specs/router" :class "hover:underline"
:sx-get "/language/specs/router" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
"router.sx"))
(td :class "px-3 py-2 text-stone-700" "Client-side route matching — Flask-style patterns, param extraction"))))))
(div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Browser")
(h2 :class "text-2xl font-semibold text-stone-800" "Reactive")
(p :class "text-stone-600"
"Browser-level support: startup lifecycle and on-demand CSS. "
(code :class "text-violet-700 text-sm" "boot.sx")
" handles page load — processing scripts, mounting content, and hydrating elements. "
(code :class "text-violet-700 text-sm" "cssx.sx")
" provides the on-demand CSS system that resolves keyword atoms into class names and injects rules as needed.")
"Fine-grained reactive primitives for client-side islands. Signals notify subscribers on change, computed values derive lazily, effects run side-effects with cleanup, and batch coalesces updates.")
(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"
@@ -145,45 +161,80 @@
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700"
(a :href "/language/specs/boot" :class "hover:underline"
:sx-get "/language/specs/boot" :sx-target "#main-panel" :sx-select "#main-panel"
(a :href "/language/specs/signals" :class "hover:underline"
:sx-get "/language/specs/signals" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
"boot.sx"))
(td :class "px-3 py-2 text-stone-700" "Browser startup lifecycle — mount, hydrate, script processing, head hoisting"))
"signals.sx"))
(td :class "px-3 py-2 text-stone-700" "Signal runtime — signal, deref, reset!, swap!, computed, effect, batch, island scope"))))))
(div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Host Interface")
(p :class "text-stone-600"
"The contract between SX and its host environment. Boundary declares what the host must provide. Forms define server-side application constructs. Page helpers offer pure data transformations for rendering.")
(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" "File")
(th :class "px-3 py-2 font-medium text-stone-600" "Role")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700"
(a :href "/language/specs/cssx" :class "hover:underline"
:sx-get "/language/specs/cssx" :sx-target "#main-panel" :sx-select "#main-panel"
(a :href "/language/specs/boundary" :class "hover:underline"
:sx-get "/language/specs/boundary" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
"cssx.sx"))
(td :class "px-3 py-2 text-stone-700" "On-demand CSS — style dictionary, keyword resolution, rule injection"))))))
"boundary.sx"))
(td :class "px-3 py-2 text-stone-700" "Language boundary — I/O primitives, page helpers, tier declarations"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700"
(a :href "/language/specs/forms" :class "hover:underline"
:sx-get "/language/specs/forms" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
"forms.sx"))
(td :class "px-3 py-2 text-stone-700" "Definition forms — defhandler, defquery, defaction, defpage"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700"
(a :href "/language/specs/page-helpers" :class "hover:underline"
:sx-get "/language/specs/page-helpers" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
"page-helpers.sx"))
(td :class "px-3 py-2 text-stone-700" "Pure data-transformation helpers for page rendering"))))))
(div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Dependency graph")
(div :class "not-prose bg-stone-100 rounded-lg p-5 mx-auto max-w-3xl"
(pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words font-mono text-stone-700"
"parser.sx (standalone — no dependencies)
";; Core
parser.sx (standalone — no dependencies)
primitives.sx (standalone — declarative registry)
special-forms.sx (standalone — declarative registry)
eval.sx depends on: parser, primitives, special-forms
render.sx (standalone — shared registries)
;; Adapters
adapter-dom.sx depends on: render, eval
adapter-html.sx depends on: render, eval
adapter-sx.sx depends on: render, eval
adapter-async.sx depends on: adapter-html, adapter-sx, eval
;; Browser Runtime
engine.sx depends on: eval, adapter-dom
orchestration.sx depends on: engine, adapter-dom
cssx.sx depends on: render
boot.sx depends on: cssx, orchestration, adapter-dom, render
boot.sx depends on: orchestration, adapter-dom, render
router.sx (standalone — pure string/list ops)
;; Extensions (optional — loaded only when target requests them)
continuations.sx depends on: eval (optional)
callcc.sx depends on: eval (optional)
;; Reactive
signals.sx depends on: eval
;; Spec modules (optional — loaded via --spec-modules)
deps.sx depends on: eval (optional)
router.sx (standalone — pure string/list ops)")))
;; Host Interface
boundary.sx (standalone — declarative contract)
forms.sx depends on: eval
page-helpers.sx (standalone — declarative registry)
;; Extensions (optional)
continuations.sx depends on: eval
callcc.sx depends on: eval
types.sx depends on: eval, primitives
deps.sx depends on: eval")))
(div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Extensions")
@@ -193,8 +244,7 @@ router.sx (standalone — pure string/list ops)")))
(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" "File")
(th :class "px-3 py-2 font-medium text-stone-600" "Role")
(th :class "px-3 py-2 font-medium text-stone-600" "Recommended targets")))
(th :class "px-3 py-2 font-medium text-stone-600" "Role")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700"
@@ -202,16 +252,28 @@ router.sx (standalone — pure string/list ops)")))
:sx-get "/language/specs/continuations" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
"continuations.sx"))
(td :class "px-3 py-2 text-stone-700" "Delimited continuations — shift/reset")
(td :class "px-3 py-2 text-stone-500" "All targets"))
(td :class "px-3 py-2 text-stone-700" "Delimited continuations — shift/reset"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700"
(a :href "/language/specs/callcc" :class "hover:underline"
:sx-get "/language/specs/callcc" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
"callcc.sx"))
(td :class "px-3 py-2 text-stone-700" "Full first-class continuations — call/cc")
(td :class "px-3 py-2 text-stone-500" "Scheme, Haskell"))))))
(td :class "px-3 py-2 text-stone-700" "Full first-class continuations — call/cc"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700"
(a :href "/language/specs/types" :class "hover:underline"
:sx-get "/language/specs/types" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
"types.sx"))
(td :class "px-3 py-2 text-stone-700" "Gradual type system — registration-time checking, zero runtime cost"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700"
(a :href "/language/specs/deps" :class "hover:underline"
:sx-get "/language/specs/deps" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
"deps.sx"))
(td :class "px-3 py-2 text-stone-700" "Component dependency analysis — bundling, IO detection, CSS scoping"))))))
(div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Self-hosting")
@@ -235,11 +297,17 @@ router.sx (standalone — pure string/list ops)")))
(p :class "text-stone-600 mb-6"
(case spec-title
"Core Language"
"The core specification defines the language itself — parsing, evaluation, primitives, and shared rendering definitions. These four files are platform-independent and sufficient to implement SX on any target."
"Adapters & Engine"
"Adapters connect the core language to specific environments. Each adapter takes evaluated expression trees and produces output for its target. The engine adds browser-side fetch/swap behaviour, split into pure logic and browser orchestration."
"Browser"
"Browser-level support: the startup lifecycle that boots SX in the browser, and the on-demand CSS system that resolves keyword atoms into Tailwind-compatible class names."
"The core specification defines the language itself — parsing, evaluation, primitives, special forms, and shared rendering definitions. These five files are platform-independent and sufficient to implement SX on any target."
"Adapters"
"Adapters connect the core language to specific environments. Each adapter takes evaluated expression trees and produces output for its target — DOM nodes, HTML strings, SX wire format, or async-aware server rendering."
"Browser Runtime"
"The browser runtime handles the client-side lifecycle: parsing triggers, making requests, swapping content, managing history, and booting the page. Split into pure logic (engine), browser wiring (orchestration), startup (boot), and URL matching (router)."
"Reactive System"
"Fine-grained reactive primitives for client-side islands. Signals, computed values, effects, and batching — the reactive graph that powers L2-L3 interactivity without a virtual DOM."
"Host Interface"
"The contract between SX and its host environment. Boundary declarations specify what the host must provide, forms define server-side application constructs, and page helpers offer pure data transformations."
"Extensions"
"Optional bolt-on specifications that extend the core language. Bootstrappers include them only when the target requests them. Code that doesn't use extensions pays zero cost."
:else ""))
(div :class "space-y-8"
(map (fn (spec)

View File

@@ -258,6 +258,7 @@
"zero-tooling" (~essay-zero-tooling)
"react-is-hypermedia" (~essay-react-is-hypermedia)
"hegelian-synthesis" (~essay-hegelian-synthesis)
"the-art-chain" (~essay-the-art-chain)
:else (~essays-index-content))))
;; ---------------------------------------------------------------------------
@@ -322,48 +323,41 @@
:auth :public
:layout :sx-docs
:content (~sx-doc :path (str "/language/specs/" slug)
(case slug
"core" (~spec-overview-content
:spec-title "Core Language"
:spec-files (map (fn (item)
(dict :title (get item "title") :desc (get item "desc")
:prose (get item "prose")
:filename (get item "filename") :href (str "/language/specs/" (get item "slug"))
:source (read-spec-file (get item "filename"))))
core-spec-items))
"adapters" (~spec-overview-content
:spec-title "Adapters & Engine"
:spec-files (map (fn (item)
(dict :title (get item "title") :desc (get item "desc")
:prose (get item "prose")
:filename (get item "filename") :href (str "/language/specs/" (get item "slug"))
:source (read-spec-file (get item "filename"))))
adapter-spec-items))
"browser" (~spec-overview-content
:spec-title "Browser"
:spec-files (map (fn (item)
(dict :title (get item "title") :desc (get item "desc")
:prose (get item "prose")
:filename (get item "filename") :href (str "/language/specs/" (get item "slug"))
:source (read-spec-file (get item "filename"))))
browser-spec-items))
"extensions" (~spec-overview-content
:spec-title "Extensions"
:spec-files (map (fn (item)
(dict :title (get item "title") :desc (get item "desc")
:prose (get item "prose")
:filename (get item "filename") :href (str "/language/specs/" (get item "slug"))
:source (read-spec-file (get item "filename"))))
extension-spec-items))
:else (let ((spec (find-spec slug)))
(if spec
(~spec-detail-content
:spec-title (get spec "title")
:spec-desc (get spec "desc")
:spec-filename (get spec "filename")
:spec-source (read-spec-file (get spec "filename"))
:spec-prose (get spec "prose"))
(~spec-not-found :slug slug))))))
(let ((make-spec-files (fn (items)
(map (fn (item)
(dict :title (get item "title") :desc (get item "desc")
:prose (get item "prose")
:filename (get item "filename") :href (str "/language/specs/" (get item "slug"))
:source (read-spec-file (get item "filename"))))
items))))
(case slug
"core" (~spec-overview-content
:spec-title "Core Language"
:spec-files (make-spec-files core-spec-items))
"adapters" (~spec-overview-content
:spec-title "Adapters"
:spec-files (make-spec-files adapter-spec-items))
"browser" (~spec-overview-content
:spec-title "Browser Runtime"
:spec-files (make-spec-files browser-spec-items))
"reactive" (~spec-overview-content
:spec-title "Reactive System"
:spec-files (make-spec-files reactive-spec-items))
"host" (~spec-overview-content
:spec-title "Host Interface"
:spec-files (make-spec-files host-spec-items))
"extensions" (~spec-overview-content
:spec-title "Extensions"
:spec-files (make-spec-files extension-spec-items))
:else (let ((spec (find-spec slug)))
(if spec
(~spec-detail-content
:spec-title (get spec "title")
:spec-desc (get spec "desc")
:spec-filename (get spec "filename")
:spec-source (read-spec-file (get spec "filename"))
:spec-prose (get spec "prose"))
(~spec-not-found :slug slug)))))))
;; ---------------------------------------------------------------------------
;; Bootstrappers section