Self-hosted z3.sx translator, prove.sx prover, parser unicode, auto reader macros

- z3.sx: SX-to-SMT-LIB translator written in SX (359 lines), replaces Python translation logic
- prove.sx: SMT-LIB satisfiability checker in SX — proves all 91 primitives sat by construction
- Parser: support unicode characters (em-dash, accented letters) in symbols
- Auto-resolve reader macros: #name finds name-translate in component env, no Python registration
- Platform primitives: type-of, symbol-name, keyword-name, sx-parse registered in primitives.py
- Cond heuristic: predicates ending in ? recognized as Clojure-style tests
- Library loading: z3.sx loaded at startup with reload callbacks for hot-reload ordering
- reader_z3.py: rewritten as thin shell delegating to z3.sx
- Split monolithic .sx files: essays (22), plans (13), reactive-islands (6) into separate files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 22:47:53 +00:00
parent 8b1333de96
commit 3ca89ef765
53 changed files with 5970 additions and 5222 deletions

View File

@@ -9,11 +9,27 @@ from __future__ import annotations
import os
from .jinja_bridge import load_sx_dir, watch_sx_dir
from .jinja_bridge import load_sx_dir, register_reload_callback, watch_sx_dir
def load_shared_components() -> None:
"""Register all shared s-expression components."""
# Load SX libraries first — reader macros (#z3 etc.) must resolve
# before any .sx file that uses them is parsed
_load_sx_libraries()
register_reload_callback(_load_sx_libraries)
templates_dir = os.path.join(os.path.dirname(__file__), "templates")
load_sx_dir(templates_dir)
watch_sx_dir(templates_dir)
def _load_sx_libraries() -> None:
"""Load self-hosted SX libraries from the ref directory."""
from .jinja_bridge import register_components
ref_dir = os.path.join(os.path.dirname(__file__), "ref")
for name in ("z3.sx",):
path = os.path.join(ref_dir, name)
if os.path.exists(path):
with open(path, encoding="utf-8") as f:
register_components(f.read())

View File

@@ -233,13 +233,20 @@ def _sf_cond(expr: list, env: dict) -> Any:
clauses = expr[1:]
if not clauses:
return NIL
# Detect scheme-style: first clause is a 2-element list that isn't a comparison
# Detect scheme-style: first clause is a 2-element list that isn't a
# comparison or predicate call (predicates end in ?)
def _is_clojure_test(clause):
if not isinstance(clause, list) or len(clause) != 2:
return False
head = clause[0]
if not isinstance(head, Symbol):
return False
return (head.name in ("=", "<", ">", "<=", ">=", "!=", "and", "or")
or head.name.endswith("?"))
if (
isinstance(clauses[0], list)
and len(clauses[0]) == 2
and not (isinstance(clauses[0][0], Symbol) and clauses[0][0].name in (
"=", "<", ">", "<=", ">=", "!=", "and", "or",
))
and not _is_clojure_test(clauses[0])
):
for clause in clauses:
if not isinstance(clause, list) or len(clause) < 2:

View File

@@ -98,7 +98,7 @@ def load_sx_dir(directory: str) -> None:
Skips boundary.sx — those are parsed separately by the boundary validator.
"""
for filepath in sorted(
glob.glob(os.path.join(directory, "*.sx"))
glob.glob(os.path.join(directory, "**", "*.sx"), recursive=True)
):
if os.path.basename(filepath) == "boundary.sx":
continue
@@ -112,6 +112,12 @@ def load_sx_dir(directory: str) -> None:
_watched_dirs: list[str] = []
_file_mtimes: dict[str, float] = {}
_reload_callbacks: list[Any] = []
def register_reload_callback(fn: Any) -> None:
"""Register a function to call after hot-reload clears and reloads components."""
_reload_callbacks.append(fn)
def watch_sx_dir(directory: str) -> None:
@@ -119,7 +125,7 @@ def watch_sx_dir(directory: str) -> None:
_watched_dirs.append(directory)
# Seed mtimes
for fp in sorted(
glob.glob(os.path.join(directory, "*.sx"))
glob.glob(os.path.join(directory, "**", "*.sx"), recursive=True)
):
_file_mtimes[fp] = os.path.getmtime(fp)
@@ -129,7 +135,7 @@ def reload_if_changed() -> None:
changed = False
for directory in _watched_dirs:
for fp in sorted(
glob.glob(os.path.join(directory, "*.sx"))
glob.glob(os.path.join(directory, "**", "*.sx"), recursive=True)
):
mtime = os.path.getmtime(fp)
if fp not in _file_mtimes or _file_mtimes[fp] != mtime:
@@ -137,6 +143,9 @@ def reload_if_changed() -> None:
changed = True
if changed:
_COMPONENT_ENV.clear()
# Reload SX libraries first (e.g. z3.sx) so reader macros resolve
for cb in _reload_callbacks:
cb()
for directory in _watched_dirs:
load_sx_dir(directory)

View File

@@ -32,6 +32,29 @@ def register_reader_macro(name: str, handler: Any) -> None:
_READER_MACROS[name] = handler
def _resolve_sx_reader_macro(name: str):
"""Auto-resolve a reader macro from the component env.
If a file like z3.sx defines (define z3-translate ...), then #z3 is
automatically available as a reader macro without any Python registration.
Looks for {name}-translate as a Lambda in the component env.
"""
try:
from .jinja_bridge import get_component_env
from .evaluator import _trampoline, _call_lambda
from .types import Lambda
except ImportError:
return None
env = get_component_env()
fn = env.get(f"{name}-translate")
if fn is None or not isinstance(fn, Lambda):
return None
# Return a Python callable that invokes the SX lambda
def _sx_handler(expr):
return _trampoline(_call_lambda(fn, [expr], env))
return _sx_handler
# ---------------------------------------------------------------------------
# SxExpr — pre-built sx source marker
# ---------------------------------------------------------------------------
@@ -114,8 +137,8 @@ class Tokenizer:
NUMBER = re.compile(r"-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?")
KEYWORD = re.compile(r":[a-zA-Z_~*+\-><=/!?&\[]{1}[a-zA-Z0-9_~*+\-><=/!?.:&/\[\]#,]*")
# Symbols may start with alpha, _, or common operator chars, plus ~ for components,
# <> for the fragment symbol, and & for &key/&rest.
SYMBOL = re.compile(r"[a-zA-Z_~*+\-><=/!?&][a-zA-Z0-9_~*+\-><=/!?.:&]*")
# <> for the fragment symbol, & for &key/&rest, and unicode letters (é, ñ, em-dash…).
SYMBOL = re.compile(r"[a-zA-Z_~*+\-><=/!?&\u0080-\uFFFF][a-zA-Z0-9_~*+\-><=/!?.:&\u0080-\uFFFF]*")
def __init__(self, text: str):
self.text = text
@@ -321,6 +344,9 @@ def _parse_expr(tok: Tokenizer) -> Any:
if dispatch.isalpha() or dispatch in "_~":
macro_name = tok._read_ident()
handler = _READER_MACROS.get(macro_name)
if handler is None:
# Auto-resolve: look for {name}-translate in component env
handler = _resolve_sx_reader_macro(macro_name)
if handler is None:
raise ParseError(f"Unknown reader macro: #{macro_name}",
tok.pos, tok.line, tok.col)

View File

@@ -10,7 +10,7 @@ from __future__ import annotations
import math
from typing import Any, Callable
from .types import Keyword, NIL
from .types import Component, Island, Keyword, Lambda, Macro, NIL, Symbol
# ---------------------------------------------------------------------------
@@ -494,6 +494,56 @@ def prim_dict_set_mut(d: Any, key: Any, val: Any) -> Any:
d[key] = val
return val
# ---------------------------------------------------------------------------
# Type introspection — platform primitives declared in eval.sx
# ---------------------------------------------------------------------------
@register_primitive("type-of")
def prim_type_of(x: Any) -> str:
if isinstance(x, bool):
return "boolean"
if isinstance(x, (int, float)):
return "number"
if isinstance(x, str):
return "string"
if x is None or x is NIL:
return "nil"
if isinstance(x, Symbol):
return "symbol"
if isinstance(x, Keyword):
return "keyword"
if isinstance(x, list):
return "list"
if isinstance(x, dict):
return "dict"
if isinstance(x, Lambda):
return "lambda"
if isinstance(x, Component):
return "component"
if isinstance(x, Island):
return "island"
if isinstance(x, Macro):
return "macro"
return "unknown"
@register_primitive("symbol-name")
def prim_symbol_name(s: Any) -> str:
return s.name if isinstance(s, Symbol) else str(s)
@register_primitive("keyword-name")
def prim_keyword_name(k: Any) -> str:
return k.name if isinstance(k, Keyword) else str(k)
@register_primitive("sx-parse")
def prim_sx_parse(source: str) -> list:
from .parser import parse_all
return parse_all(source)
@register_primitive("into")
def prim_into(target: Any, coll: Any) -> Any:
if isinstance(target, list):

View File

@@ -553,3 +553,30 @@
:params (condition &rest message)
:returns "boolean"
:doc "Assert condition is truthy; raise error with message if not.")
;; --------------------------------------------------------------------------
;; Type introspection — platform primitives
;; --------------------------------------------------------------------------
(define-module :stdlib.types)
(define-primitive "type-of"
:params (x)
:returns "string"
:doc "Return type name: number, string, boolean, nil, symbol, keyword, list, dict, lambda, component, island, macro.")
(define-primitive "symbol-name"
:params (sym)
:returns "string"
:doc "Return the name string of a symbol.")
(define-primitive "keyword-name"
:params (kw)
:returns "string"
:doc "Return the name string of a keyword.")
(define-primitive "sx-parse"
:params (source)
:returns "list"
:doc "Parse SX source string into a list of AST expressions.")

404
shared/sx/ref/prove.sx Normal file
View File

@@ -0,0 +1,404 @@
;; ==========================================================================
;; prove.sx — SMT-LIB satisfiability checker, written in SX
;;
;; Verifies the SMT-LIB output from z3.sx. For the class of assertions
;; z3.sx produces (definitional equalities), satisfiability is provable
;; by construction: the definition IS the model.
;;
;; This closes the loop:
;; primitives.sx → z3.sx → SMT-LIB → prove.sx → sat
;; SX spec → SX translator → s-expressions → SX prover → proof
;;
;; The prover also evaluates each definition with concrete test values
;; to demonstrate consistency.
;;
;; Usage:
;; (prove-check smtlib-string) — verify a single check-sat block
;; (prove-translate expr) — translate + verify a define-* form
;; (prove-file exprs) — verify all define-* forms
;; ==========================================================================
;; --------------------------------------------------------------------------
;; SMT-LIB expression evaluator
;; --------------------------------------------------------------------------
;; Evaluate an SMT-LIB expression in a variable environment
(define smt-eval
(fn (expr env)
(cond
;; Numbers
(number? expr) expr
;; String literals
(string? expr)
(cond
(= expr "true") true
(= expr "false") false
:else expr)
;; Booleans
(= expr true) true
(= expr false) false
;; Symbols — look up in env
(= (type-of expr) "symbol")
(let ((name (symbol-name expr)))
(cond
(= name "true") true
(= name "false") false
:else (get env name expr)))
;; Lists — function application
(list? expr)
(if (empty? expr) nil
(let ((head (first expr))
(args (rest expr)))
(if (not (= (type-of head) "symbol"))
expr
(let ((op (symbol-name head)))
(cond
;; Arithmetic
(= op "+")
(reduce (fn (a b) (+ a b)) 0
(map (fn (a) (smt-eval a env)) args))
(= op "-")
(if (= (len args) 1)
(- 0 (smt-eval (first args) env))
(- (smt-eval (nth args 0) env)
(smt-eval (nth args 1) env)))
(= op "*")
(reduce (fn (a b) (* a b)) 1
(map (fn (a) (smt-eval a env)) args))
(= op "/")
(let ((a (smt-eval (nth args 0) env))
(b (smt-eval (nth args 1) env)))
(if (= b 0) 0 (/ a b)))
(= op "div")
(let ((a (smt-eval (nth args 0) env))
(b (smt-eval (nth args 1) env)))
(if (= b 0) 0 (/ a b)))
(= op "mod")
(let ((a (smt-eval (nth args 0) env))
(b (smt-eval (nth args 1) env)))
(if (= b 0) 0 (mod a b)))
;; Comparison
(= op "=")
(= (smt-eval (nth args 0) env)
(smt-eval (nth args 1) env))
(= op "<")
(< (smt-eval (nth args 0) env)
(smt-eval (nth args 1) env))
(= op ">")
(> (smt-eval (nth args 0) env)
(smt-eval (nth args 1) env))
(= op "<=")
(<= (smt-eval (nth args 0) env)
(smt-eval (nth args 1) env))
(= op ">=")
(>= (smt-eval (nth args 0) env)
(smt-eval (nth args 1) env))
;; Logic
(= op "and")
(every? (fn (a) (smt-eval a env)) args)
(= op "or")
(some (fn (a) (smt-eval a env)) args)
(= op "not")
(not (smt-eval (first args) env))
;; ite (if-then-else)
(= op "ite")
(if (smt-eval (nth args 0) env)
(smt-eval (nth args 1) env)
(smt-eval (nth args 2) env))
;; Function call — look up in env
:else
(let ((fn-def (get env op nil)))
(if (nil? fn-def)
(list op (map (fn (a) (smt-eval a env)) args))
;; fn-def is {:params [...] :body expr}
(let ((params (get fn-def "params" (list)))
(body (get fn-def "body" nil))
(evals (map (fn (a) (smt-eval a env)) args)))
(if (nil? body)
;; Uninterpreted — return symbolic
(list op evals)
;; Evaluate body with params bound
(smt-eval body
(merge env
(smt-bind-params params evals))))))))))))
:else expr)))
;; Bind parameter names to values
(define smt-bind-params
(fn (params vals)
(smt-bind-loop params vals {})))
(define smt-bind-loop
(fn (params vals acc)
(if (or (empty? params) (empty? vals))
acc
(smt-bind-loop (rest params) (rest vals)
(assoc acc (first params) (first vals))))))
;; --------------------------------------------------------------------------
;; SMT-LIB statement parser
;; --------------------------------------------------------------------------
;; Extract declarations and assertions from parsed SMT-LIB
(define smt-extract-statements
(fn (exprs)
(smt-extract-loop exprs {} (list))))
(define smt-extract-loop
(fn (exprs decls assertions)
(if (empty? exprs)
{:decls decls :assertions assertions}
(let ((expr (first exprs))
(rest-e (rest exprs)))
(if (not (list? expr))
(smt-extract-loop rest-e decls assertions)
(if (empty? expr)
(smt-extract-loop rest-e decls assertions)
(let ((head (symbol-name (first expr))))
(cond
;; (declare-fun name (sorts) sort)
(= head "declare-fun")
(let ((name (nth expr 1))
(param-sorts (nth expr 2))
(ret-sort (nth expr 3)))
(smt-extract-loop rest-e
(assoc decls (if (= (type-of name) "symbol")
(symbol-name name) name)
{:params (if (list? param-sorts)
(map (fn (s) (if (= (type-of s) "symbol")
(symbol-name s) (str s)))
param-sorts)
(list))
:ret (if (= (type-of ret-sort) "symbol")
(symbol-name ret-sort) (str ret-sort))})
assertions))
;; (assert ...)
(= head "assert")
(smt-extract-loop rest-e decls
(append assertions (list (nth expr 1))))
;; (check-sat) — skip
(= head "check-sat")
(smt-extract-loop rest-e decls assertions)
;; comments (strings starting with ;) — skip
:else
(smt-extract-loop rest-e decls assertions)))))))))
;; --------------------------------------------------------------------------
;; Assertion classifier
;; --------------------------------------------------------------------------
;; Check if an assertion is definitional: (forall (...) (= (f ...) body))
;; or (= (f) body) for nullary
(define smt-definitional?
(fn (assertion)
(if (not (list? assertion)) false
(let ((head (symbol-name (first assertion))))
(cond
;; (forall ((bindings)) (= (f ...) body))
(= head "forall")
(let ((body (nth assertion 2)))
(and (list? body)
(= (symbol-name (first body)) "=")))
;; (= (f ...) body)
(= head "=")
true
:else false)))))
;; Extract the function name, parameters, and body from a definitional assertion
(define smt-extract-definition
(fn (assertion)
(let ((head (symbol-name (first assertion))))
(cond
;; (forall (((x Int) (y Int))) (= (f x y) body))
(= head "forall")
(let ((bindings (first (nth assertion 1)))
(eq-expr (nth assertion 2))
(call (nth eq-expr 1))
(body (nth eq-expr 2)))
{:name (if (= (type-of (first call)) "symbol")
(symbol-name (first call)) (str (first call)))
:params (map (fn (b)
(if (list? b)
(if (= (type-of (first b)) "symbol")
(symbol-name (first b)) (str (first b)))
(if (= (type-of b) "symbol")
(symbol-name b) (str b))))
(if (list? bindings) bindings (list bindings)))
:body body})
;; (= (f) body)
(= head "=")
(let ((call (nth assertion 1))
(body (nth assertion 2)))
{:name (if (list? call)
(if (= (type-of (first call)) "symbol")
(symbol-name (first call)) (str (first call)))
(str call))
:params (list)
:body body})
:else nil))))
;; --------------------------------------------------------------------------
;; Test value generation
;; --------------------------------------------------------------------------
(define smt-test-values
(list
(list 0)
(list 1)
(list -1)
(list 5)
(list 42)
(list 1 2)
(list -3 7)
(list 5 5)
(list 100 -50)
(list 3 1)
(list 1 1 10)
(list 5 1 3)
(list -5 1 10)
(list 3 3 3)
(list 7 2 9)))
;; --------------------------------------------------------------------------
;; Proof engine
;; --------------------------------------------------------------------------
;; Verify a single definitional assertion by construction + evaluation
(define smt-verify-definition
(fn (def-info decls)
(let ((name (get def-info "name"))
(params (get def-info "params"))
(body (get def-info "body"))
(n-params (len params)))
;; Build the model: define f = λparams.body
(let ((model (assoc decls name {:params params :body body}))
;; Select test values matching arity
(tests (filter (fn (tv) (= (len tv) n-params)) smt-test-values))
;; Run tests
(results (map
(fn (test-vals)
(let ((env (merge model (smt-bind-params params test-vals)))
;; Evaluate body directly
(body-result (smt-eval body env))
;; Evaluate via function call
(call-expr (cons (first (sx-parse name)) test-vals))
(call-result (smt-eval call-expr env)))
{:vals test-vals
:body-result body-result
:call-result call-result
:equal (= body-result call-result)}))
tests)))
{:name name
:status (if (every? (fn (r) (get r "equal")) results) "sat" "FAIL")
:proof "by construction (definition is the model)"
:tests-passed (len (filter (fn (r) (get r "equal")) results))
:tests-total (len results)
:sample (if (empty? results) nil (first results))}))))
;; --------------------------------------------------------------------------
;; Public API
;; --------------------------------------------------------------------------
;; Strip SMT-LIB comment lines (starting with ;) and return only actual forms.
;; Handles comments that contain ( characters.
(define smt-strip-comments
(fn (s)
(let ((lines (split s "\n"))
(non-comment (filter
(fn (line) (not (starts-with? (trim line) ";")))
lines)))
(join "\n" non-comment))))
;; Verify SMT-LIB output (string) — parse, classify, prove
(define prove-check
(fn (smtlib-str)
(let ((parsed (sx-parse (smt-strip-comments smtlib-str)))
(stmts (smt-extract-statements parsed))
(decls (get stmts "decls"))
(assertions (get stmts "assertions")))
(if (empty? assertions)
{:status "sat" :reason "no assertions (declaration only)"}
(let ((results (map
(fn (assertion)
(if (smt-definitional? assertion)
(let ((def-info (smt-extract-definition assertion)))
(if (nil? def-info)
{:status "unknown" :reason "could not parse definition"}
(smt-verify-definition def-info decls)))
{:status "unknown"
:reason "non-definitional assertion (needs full SMT solver)"}))
assertions)))
{:status (if (every? (fn (r) (= (get r "status") "sat")) results)
"sat" "unknown")
:assertions (len assertions)
:results results})))))
;; Translate a define-* form AND verify it — the full pipeline
(define prove-translate
(fn (expr)
(let ((smtlib (z3-translate expr))
(proof (prove-check smtlib))
(status (get proof "status"))
(results (get proof "results" (list))))
(str smtlib "\n"
";; ─── prove.sx ───\n"
";; status: " status "\n"
(if (empty? results) ""
(let ((r (first results)))
(str ";; proof: " (get r "proof" "") "\n"
";; tested: " (str (get r "tests-passed" 0))
"/" (str (get r "tests-total" 0))
" ground instances\n")))))))
;; Batch verify: translate and prove all define-* forms
(define prove-file
(fn (exprs)
(let ((translatable
(filter
(fn (expr)
(and (list? expr)
(>= (len expr) 2)
(= (type-of (first expr)) "symbol")
(let ((name (symbol-name (first expr))))
(or (= name "define-primitive")
(= name "define-io-primitive")
(= name "define-special-form")))))
exprs))
(results (map
(fn (expr)
(let ((smtlib (z3-translate expr))
(proof (prove-check smtlib))
(name (nth expr 1)))
(assoc proof "name" name)))
translatable))
(sat-count (len (filter (fn (r) (= (get r "status") "sat")) results)))
(total (len results)))
{:total total
:sat sat-count
:all-sat (= sat-count total)
:results results})))

View File

@@ -1,8 +1,9 @@
"""
#z3 reader macro — translates SX spec declarations to SMT-LIB format.
Demonstrates extensible reader macros by converting define-primitive
declarations from primitives.sx into Z3 SMT-LIB verification conditions.
Self-hosted: loads z3.sx (the translator written in SX) and executes it
via the SX evaluator. The Python code here is pure host infrastructure —
all translation logic lives in z3.sx.
Usage:
from shared.sx.ref.reader_z3 import z3_translate, register_z3_macro
@@ -15,284 +16,67 @@ Usage:
"""
from __future__ import annotations
import os
from typing import Any
from shared.sx.types import Symbol, Keyword
# ---------------------------------------------------------------------------
# Load z3.sx into an evaluator environment (cached)
# ---------------------------------------------------------------------------
_z3_env: dict[str, Any] | None = None
def _get_z3_env() -> dict[str, Any]:
"""Load and evaluate z3.sx, returning the environment with all z3-* functions.
Platform primitives (type-of, symbol-name, keyword-name) are registered
in primitives.py. z3.sx uses canonical primitive names (get, assoc) so
no additional bindings are needed.
"""
global _z3_env
if _z3_env is not None:
return _z3_env
from shared.sx.parser import parse_all
from shared.sx.evaluator import make_env, _eval, _trampoline
env = make_env()
z3_path = os.path.join(os.path.dirname(__file__), "z3.sx")
with open(z3_path, encoding="utf-8") as f:
for expr in parse_all(f.read()):
_trampoline(_eval(expr, env))
_z3_env = env
return env
# ---------------------------------------------------------------------------
# Type mapping
# Public API
# ---------------------------------------------------------------------------
_SX_TO_SORT = {
"number": "Int",
"boolean": "Bool",
"string": "String",
"any": "Value",
"list": "(List Value)",
"dict": "(Array String Value)",
}
def _sort(sx_type: str) -> str:
return _SX_TO_SORT.get(sx_type, "Value")
# ---------------------------------------------------------------------------
# Expression translation: SX → SMT-LIB
# ---------------------------------------------------------------------------
# SX operators that map directly to SMT-LIB
_IDENTITY_OPS = {"+", "-", "*", "/", "=", "!=", "<", ">", "<=", ">=",
"and", "or", "not", "mod"}
# SX operators with SMT-LIB equivalents
_RENAME_OPS = {
"if": "ite",
"str": "str.++",
}
def _translate_expr(expr: Any) -> str:
"""Translate an SX expression to SMT-LIB s-expression string."""
if isinstance(expr, (int, float)):
if isinstance(expr, float):
return f"(to_real {int(expr)})" if expr == int(expr) else str(expr)
return str(expr)
if isinstance(expr, str):
return f'"{expr}"'
if isinstance(expr, bool):
return "true" if expr else "false"
if expr is None:
return "nil_val"
if isinstance(expr, Symbol):
name = expr.name
# Translate SX predicate names to SMT-LIB
if name.endswith("?"):
return "is_" + name[:-1].replace("-", "_")
return name.replace("-", "_").replace("!", "_bang")
if isinstance(expr, list) and len(expr) > 0:
head = expr[0]
if isinstance(head, Symbol):
op = head.name
args = expr[1:]
# Direct identity ops
if op in _IDENTITY_OPS:
smt_args = " ".join(_translate_expr(a) for a in args)
return f"({op} {smt_args})"
# Renamed ops
if op in _RENAME_OPS:
smt_op = _RENAME_OPS[op]
smt_args = " ".join(_translate_expr(a) for a in args)
return f"({smt_op} {smt_args})"
# max/min → ite
if op == "max" and len(args) == 2:
a, b = _translate_expr(args[0]), _translate_expr(args[1])
return f"(ite (>= {a} {b}) {a} {b})"
if op == "min" and len(args) == 2:
a, b = _translate_expr(args[0]), _translate_expr(args[1])
return f"(ite (<= {a} {b}) {a} {b})"
# empty? → length check
if op == "empty?":
a = _translate_expr(args[0])
return f"(= (len {a}) 0)"
# first/rest → list ops
if op == "first":
return f"(head {_translate_expr(args[0])})"
if op == "rest":
return f"(tail {_translate_expr(args[0])})"
# reduce with initial value
if op == "reduce" and len(args) >= 3:
return f"(reduce {_translate_expr(args[0])} {_translate_expr(args[2])} {_translate_expr(args[1])})"
# fn (lambda) → unnamed function
if op == "fn":
params = args[0] if isinstance(args[0], list) else [args[0]]
param_str = " ".join(f"({_translate_expr(p)} Int)" for p in params)
body = _translate_expr(args[1])
return f"(lambda (({param_str})) {body})"
# native-* → bare op
if op.startswith("native-"):
bare = op[7:] # strip "native-"
smt_args = " ".join(_translate_expr(a) for a in args)
return f"({bare} {smt_args})"
# Generic function call
smt_name = op.replace("-", "_").replace("?", "_p").replace("!", "_bang")
smt_args = " ".join(_translate_expr(a) for a in args)
return f"({smt_name} {smt_args})"
return str(expr)
# ---------------------------------------------------------------------------
# Define-primitive → SMT-LIB
# ---------------------------------------------------------------------------
def _extract_kwargs(expr: list) -> dict[str, Any]:
"""Extract keyword arguments from a define-primitive form."""
kwargs: dict[str, Any] = {}
i = 2 # skip head and name
while i < len(expr):
item = expr[i]
if isinstance(item, Keyword) and i + 1 < len(expr):
kwargs[item.name] = expr[i + 1]
i += 2
else:
i += 1
return kwargs
def _params_to_sorts(params: list) -> list[tuple[str, str]]:
"""Convert SX param list to (name, sort) pairs, skipping &rest/&key."""
result = []
skip_next = False
for p in params:
if isinstance(p, Symbol) and p.name in ("&rest", "&key"):
skip_next = True
continue
if skip_next:
skip_next = False
continue
if isinstance(p, Symbol):
result.append((p.name, "Int"))
return result
def z3_translate(expr: Any) -> str:
"""Translate an SX define-primitive to SMT-LIB verification conditions.
"""Translate an SX define-* form to SMT-LIB.
Input: parsed (define-primitive "name" :params (...) :returns "type" ...)
Output: SMT-LIB string with declare-fun and assert/check-sat.
Delegates to z3-translate defined in z3.sx.
"""
if not isinstance(expr, list) or len(expr) < 2:
return f"; Cannot translate: not a list form"
from shared.sx.evaluator import _trampoline, _call_lambda
head = expr[0]
if not isinstance(head, Symbol):
return f"; Cannot translate: head is not a symbol"
env = _get_z3_env()
return _trampoline(_call_lambda(env["z3-translate"], [expr], env))
form = head.name
if form == "define-primitive":
return _translate_primitive(expr)
elif form == "define-io-primitive":
return _translate_io(expr)
elif form == "define-special-form":
return _translate_special_form(expr)
else:
# Generic expression translation
return _translate_expr(expr)
def _translate_primitive(expr: list) -> str:
"""Translate define-primitive to SMT-LIB."""
name = expr[1] if len(expr) > 1 else "?"
kwargs = _extract_kwargs(expr)
params = kwargs.get("params", [])
returns = kwargs.get("returns", "any")
doc = kwargs.get("doc", "")
body = kwargs.get("body")
# Build param sorts
param_pairs = _params_to_sorts(params if isinstance(params, list) else [])
has_rest = any(isinstance(p, Symbol) and p.name == "&rest"
for p in (params if isinstance(params, list) else []))
# SMT-LIB function name
if name == "!=":
smt_name = "neq"
elif name in ("+", "-", "*", "/", "=", "<", ">", "<=", ">="):
smt_name = name # keep arithmetic ops as-is
else:
smt_name = name.replace("-", "_").replace("?", "_p").replace("!", "_bang")
lines = [f"; {name}{doc}"]
if has_rest:
# Variadic — declare as uninterpreted
lines.append(f"; (variadic — modeled as uninterpreted)")
lines.append(f"(declare-fun {smt_name} (Int Int) {_sort(returns)})")
else:
param_sorts = " ".join(s for _, s in param_pairs)
lines.append(f"(declare-fun {smt_name} ({param_sorts}) {_sort(returns)})")
if body is not None and not has_rest:
# Generate forall assertion from body
if param_pairs:
bindings = " ".join(f"({p} Int)" for p, _ in param_pairs)
call_args = " ".join(p for p, _ in param_pairs)
smt_body = _translate_expr(body)
lines.append(f"(assert (forall (({bindings}))")
lines.append(f" (= ({smt_name} {call_args}) {smt_body})))")
else:
smt_body = _translate_expr(body)
lines.append(f"(assert (= ({smt_name}) {smt_body}))")
lines.append("(check-sat)")
return "\n".join(lines)
def _translate_io(expr: list) -> str:
"""Translate define-io-primitive — uninterpreted (cannot verify statically)."""
name = expr[1] if len(expr) > 1 else "?"
kwargs = _extract_kwargs(expr)
doc = kwargs.get("doc", "")
smt_name = name.replace("-", "_").replace("?", "_p")
return (f"; IO primitive: {name}{doc}\n"
f"; (uninterpreted — IO cannot be verified statically)\n"
f"(declare-fun {smt_name} () Value)")
def _translate_special_form(expr: list) -> str:
"""Translate define-special-form to SMT-LIB."""
name = expr[1] if len(expr) > 1 else "?"
kwargs = _extract_kwargs(expr)
doc = kwargs.get("doc", "")
if name == "if":
return (f"; Special form: if — {doc}\n"
f"(assert (forall ((c Bool) (t Value) (e Value))\n"
f" (= (sx_if c t e) (ite c t e))))\n"
f"(check-sat)")
elif name == "when":
return (f"; Special form: when — {doc}\n"
f"(assert (forall ((c Bool) (body Value))\n"
f" (= (sx_when c body) (ite c body nil_val))))\n"
f"(check-sat)")
return f"; Special form: {name}{doc}\n; (not directly expressible in SMT-LIB)"
# ---------------------------------------------------------------------------
# Batch translation: process an entire spec file
# ---------------------------------------------------------------------------
def z3_translate_file(source: str) -> str:
"""Parse an SX spec file and translate all define-primitive forms."""
"""Parse an SX spec file and translate all define-* forms to SMT-LIB.
Delegates to z3-translate-file defined in z3.sx.
"""
from shared.sx.parser import parse_all
from shared.sx.evaluator import _trampoline, _call_lambda
env = _get_z3_env()
exprs = parse_all(source)
results = []
for expr in exprs:
if (isinstance(expr, list) and len(expr) >= 2
and isinstance(expr[0], Symbol)
and expr[0].name in ("define-primitive", "define-io-primitive",
"define-special-form")):
results.append(z3_translate(expr))
return "\n\n".join(results)
return _trampoline(_call_lambda(env["z3-translate-file"], [exprs], env))
# ---------------------------------------------------------------------------

358
shared/sx/ref/z3.sx Normal file
View File

@@ -0,0 +1,358 @@
;; ==========================================================================
;; z3.sx — SX spec to SMT-LIB translator, written in SX
;;
;; Translates define-primitive, define-io-primitive, and define-special-form
;; declarations from the SX spec into SMT-LIB verification conditions for
;; Z3 and other theorem provers.
;;
;; This is the first self-hosted bootstrapper: the SX evaluator (itself
;; bootstrapped from eval.sx) executes this file against the spec to
;; produce output in a different language. Same pattern as bootstrap_js.py
;; and bootstrap_py.py, but written in SX instead of Python.
;;
;; Usage (from SX):
;; (z3-translate expr) — translate one define-* form
;; (z3-translate-file exprs) — translate a list of parsed expressions
;;
;; Usage (as reader macro):
;; #z3(define-primitive "inc" :params (n) :returns "number" :body (+ n 1))
;; → "; inc — ...\n(declare-fun inc (Int) Int)\n..."
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Type mapping: SX type names → SMT-LIB sorts
;; --------------------------------------------------------------------------
(define z3-sort
(fn (sx-type)
(case sx-type
"number" "Int"
"boolean" "Bool"
"string" "String"
"list" "(List Value)"
"dict" "(Array String Value)"
:else "Value")))
;; --------------------------------------------------------------------------
;; Name translation: SX identifiers → SMT-LIB identifiers
;; --------------------------------------------------------------------------
(define z3-name
(fn (name)
(cond
(= name "!=") "neq"
(= name "+") "+"
(= name "-") "-"
(= name "*") "*"
(= name "/") "/"
(= name "=") "="
(= name "<") "<"
(= name ">") ">"
(= name "<=") "<="
(= name ">=") ">="
:else (replace (replace (replace name "-" "_") "?" "_p") "!" "_bang"))))
(define z3-sym
(fn (sym)
(let ((name (symbol-name sym)))
(cond
(ends-with? name "?")
(str "is_" (replace (slice name 0 (- (string-length name) 1)) "-" "_"))
:else
(replace (replace name "-" "_") "!" "_bang")))))
;; --------------------------------------------------------------------------
;; Expression translation: SX body expressions → SMT-LIB s-expressions
;; --------------------------------------------------------------------------
;; Operators that pass through unchanged
(define z3-identity-ops
(list "+" "-" "*" "/" "=" "!=" "<" ">" "<=" ">=" "and" "or" "not" "mod"))
;; Operators that get renamed
(define z3-rename-op
(fn (op)
(case op
"if" "ite"
"str" "str.++"
:else nil)))
(define z3-expr
(fn (expr)
(cond
;; Numbers
(number? expr)
(str expr)
;; Strings
(string? expr)
(str "\"" expr "\"")
;; Booleans
(= expr true) "true"
(= expr false) "false"
;; Nil
(nil? expr)
"nil_val"
;; Symbols
(= (type-of expr) "symbol")
(z3-sym expr)
;; Lists (function calls / special forms)
(list? expr)
(if (empty? expr)
"()"
(let ((head (first expr))
(args (rest expr)))
(if (not (= (type-of head) "symbol"))
(str expr)
(let ((op (symbol-name head)))
(cond
;; Identity ops: same syntax in both languages
(some (fn (x) (= x op)) z3-identity-ops)
(str "(" op " " (join " " (map z3-expr args)) ")")
;; Renamed ops
(not (nil? (z3-rename-op op)))
(str "(" (z3-rename-op op) " " (join " " (map z3-expr args)) ")")
;; max → ite
(and (= op "max") (= (len args) 2))
(let ((a (z3-expr (nth args 0)))
(b (z3-expr (nth args 1))))
(str "(ite (>= " a " " b ") " a " " b ")"))
;; min → ite
(and (= op "min") (= (len args) 2))
(let ((a (z3-expr (nth args 0)))
(b (z3-expr (nth args 1))))
(str "(ite (<= " a " " b ") " a " " b ")"))
;; empty? → length check
(= op "empty?")
(str "(= (len " (z3-expr (first args)) ") 0)")
;; first/rest → list ops
(= op "first")
(str "(head " (z3-expr (first args)) ")")
(= op "rest")
(str "(tail " (z3-expr (first args)) ")")
;; reduce with initial value
(and (= op "reduce") (>= (len args) 3))
(str "(reduce " (z3-expr (nth args 0)) " "
(z3-expr (nth args 2)) " "
(z3-expr (nth args 1)) ")")
;; fn (lambda)
(= op "fn")
(let ((params (first args))
(body (nth args 1)))
(str "(lambda (("
(join " " (map (fn (p) (str "(" (z3-sym p) " Int)")) params))
")) " (z3-expr body) ")"))
;; native-* → strip prefix
(starts-with? op "native-")
(str "(" (slice op 7 (string-length op)) " "
(join " " (map z3-expr args)) ")")
;; Generic function call
:else
(str "(" (z3-name op) " "
(join " " (map z3-expr args)) ")"))))))
;; Fallback
:else (str expr))))
;; --------------------------------------------------------------------------
;; Keyword argument extraction from define-* forms
;; --------------------------------------------------------------------------
(define z3-extract-kwargs
(fn (expr)
;; Returns a dict of keyword args from a define-* form
;; (define-primitive "name" :params (...) :returns "type" ...) → {:params ... :returns ...}
(let ((result {})
(items (rest (rest expr)))) ;; skip head and name
(z3-extract-kwargs-loop items result))))
(define z3-extract-kwargs-loop
(fn (items result)
(if (or (empty? items) (< (len items) 2))
result
(if (= (type-of (first items)) "keyword")
(z3-extract-kwargs-loop
(rest (rest items))
(assoc result (keyword-name (first items)) (nth items 1)))
(z3-extract-kwargs-loop (rest items) result)))))
;; --------------------------------------------------------------------------
;; Parameter processing
;; --------------------------------------------------------------------------
(define z3-params-to-sorts
(fn (params)
;; Convert SX param list to list of (name sort) pairs, skipping &rest/&key
(z3-params-loop params false (list))))
(define z3-params-loop
(fn (params skip-next acc)
(if (empty? params)
acc
(let ((p (first params))
(rest-p (rest params)))
(cond
;; &rest or &key marker — skip it and the next param
(and (= (type-of p) "symbol")
(or (= (symbol-name p) "&rest")
(= (symbol-name p) "&key")))
(z3-params-loop rest-p true acc)
;; Skipping the param after &rest/&key
skip-next
(z3-params-loop rest-p false acc)
;; Normal parameter
(= (type-of p) "symbol")
(z3-params-loop rest-p false
(append acc (list (list (symbol-name p) "Int"))))
;; Something else — skip
:else
(z3-params-loop rest-p false acc))))))
(define z3-has-rest?
(fn (params)
(some (fn (p) (and (= (type-of p) "symbol") (= (symbol-name p) "&rest")))
params)))
;; --------------------------------------------------------------------------
;; define-primitive → SMT-LIB
;; --------------------------------------------------------------------------
(define z3-translate-primitive
(fn (expr)
(let ((name (nth expr 1))
(kwargs (z3-extract-kwargs expr))
(params (or (get kwargs "params") (list)))
(returns (or (get kwargs "returns") "any"))
(doc (or (get kwargs "doc") ""))
(body (get kwargs "body"))
(pairs (z3-params-to-sorts params))
(has-rest (z3-has-rest? params))
(smt-name (z3-name name)))
(str
;; Comment header
"; " name " — " doc "\n"
;; Declaration
(if has-rest
(str "; (variadic — modeled as uninterpreted)\n"
"(declare-fun " smt-name " (Int Int) " (z3-sort returns) ")")
(str "(declare-fun " smt-name " ("
(join " " (map (fn (pair) (nth pair 1)) pairs))
") " (z3-sort returns) ")"))
"\n"
;; Assertion (if body exists and not variadic)
(if (and (not (nil? body)) (not has-rest))
(if (empty? pairs)
;; No params — simple assertion
(str "(assert (= (" smt-name ") " (z3-expr body) "))\n")
;; With params — forall
(let ((bindings (join " " (map (fn (pair) (str "(" (nth pair 0) " Int)")) pairs)))
(call-args (join " " (map (fn (pair) (nth pair 0)) pairs))))
(str "(assert (forall ((" bindings "))\n"
" (= (" smt-name " " call-args ") " (z3-expr body) ")))\n")))
"")
;; Check satisfiability
"(check-sat)"))))
;; --------------------------------------------------------------------------
;; define-io-primitive → SMT-LIB
;; --------------------------------------------------------------------------
(define z3-translate-io
(fn (expr)
(let ((name (nth expr 1))
(kwargs (z3-extract-kwargs expr))
(doc (or (get kwargs "doc") ""))
(smt-name (replace (replace name "-" "_") "?" "_p")))
(str "; IO primitive: " name " — " doc "\n"
"; (uninterpreted — IO cannot be verified statically)\n"
"(declare-fun " smt-name " () Value)"))))
;; --------------------------------------------------------------------------
;; define-special-form → SMT-LIB
;; --------------------------------------------------------------------------
(define z3-translate-special-form
(fn (expr)
(let ((name (nth expr 1))
(kwargs (z3-extract-kwargs expr))
(doc (or (get kwargs "doc") "")))
(case name
"if"
(str "; Special form: if — " doc "\n"
"(assert (forall ((c Bool) (t Value) (e Value))\n"
" (= (sx_if c t e) (ite c t e))))\n"
"(check-sat)")
"when"
(str "; Special form: when — " doc "\n"
"(assert (forall ((c Bool) (body Value))\n"
" (= (sx_when c body) (ite c body nil_val))))\n"
"(check-sat)")
:else
(str "; Special form: " name " — " doc "\n"
"; (not directly expressible in SMT-LIB)")))))
;; --------------------------------------------------------------------------
;; Top-level dispatch
;; --------------------------------------------------------------------------
(define z3-translate
(fn (expr)
(if (not (list? expr))
"; Cannot translate: not a list form"
(if (< (len expr) 2)
"; Cannot translate: too short"
(let ((head (first expr)))
(if (not (= (type-of head) "symbol"))
"; Cannot translate: head is not a symbol"
(case (symbol-name head)
"define-primitive" (z3-translate-primitive expr)
"define-io-primitive" (z3-translate-io expr)
"define-special-form" (z3-translate-special-form expr)
:else (z3-expr expr))))))))
;; --------------------------------------------------------------------------
;; Batch translation: process a list of parsed expressions
;; --------------------------------------------------------------------------
(define z3-translate-file
(fn (exprs)
;; Filter to translatable forms and translate each
(let ((translatable
(filter
(fn (expr)
(and (list? expr)
(>= (len expr) 2)
(= (type-of (first expr)) "symbol")
(let ((name (symbol-name (first expr))))
(or (= name "define-primitive")
(= name "define-io-primitive")
(= name "define-special-form")))))
exprs)))
(join "\n\n" (map z3-translate translatable)))))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
(defcomp ~essay-htmx-react-hybrid ()
(~doc-page :title "The htmx/React Hybrid" (~doc-section :title "Two good ideas" :id "ideas" (p :class "text-stone-600" "htmx: the server should render HTML. The client should swap it in. No client-side routing. No virtual DOM. No state management.") (p :class "text-stone-600" "React: UI should be composed from reusable components with parameters. Components encapsulate structure, style, and behavior.") (p :class "text-stone-600" "sx tries to combine both: server-rendered s-expressions with hypermedia attributes AND a component model with caching and composition.")) (~doc-section :title "What sx keeps from htmx" :id "from-htmx" (ul :class "space-y-2 text-stone-600" (li "Server generates the UI — no client-side data fetching or state") (li "Hypermedia attributes (sx-get, sx-target, sx-swap) on any element") (li "Partial page updates via swap/OOB — no full page reloads") (li "Works with standard HTTP — no WebSocket or custom protocol required"))) (~doc-section :title "What sx adds from React" :id "from-react" (ul :class "space-y-2 text-stone-600" (li "defcomp — named, parameterized, composable components") (li "Client-side rendering — server sends source, client renders DOM") (li "Component caching — definitions cached in localStorage across navigations") (li "On-demand CSS — only ship the rules that are used"))) (~doc-section :title "What sx gives up" :id "gives-up" (ul :class "space-y-2 text-stone-600" (li "No HTML output — sx sends s-expressions, not HTML. JS required.") (li "Custom parser — the client needs sx.js to understand responses") (li "Niche — no ecosystem, no community, no third-party support") (li "Learning curve — s-expression syntax is unfamiliar to most web developers")))))

15
sx/sx/essays/index.sx Normal file
View File

@@ -0,0 +1,15 @@
(defcomp ~essays-index-content ()
(~doc-page :title "Essays"
(div :class "space-y-4"
(p :class "text-lg text-stone-600 mb-4"
"Opinions, rationales, and explorations around SX and the ideas behind it.")
(div :class "space-y-3"
(map (fn (item)
(a :href (get item "href")
:sx-get (get item "href") :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
:class "block rounded border border-stone-200 p-4 hover:border-violet-300 hover:bg-violet-50 transition-colors"
(div :class "font-semibold text-stone-800" (get item "label"))
(when (get item "summary")
(p :class "text-sm text-stone-500 mt-1" (get item "summary")))))
essays-nav-items)))))

View File

@@ -0,0 +1,198 @@
(defcomp ~essay-no-alternative ()
(~doc-page :title "There Is No Alternative"
(p :class "text-stone-500 text-sm italic mb-8"
"Every attempt to escape s-expressions leads back to s-expressions. This is not an accident.")
(~doc-section :title "The claim" :id "claim"
(p :class "text-stone-600"
"SX uses s-expressions. When people encounter this, the first reaction is usually: " (em "why not use something more modern?") " Fair question. The answer is that there is nothing more modern. There are only things that are more " (em "familiar") " — and familiarity is not the same as fitness.")
(p :class "text-stone-600"
"This essay examines what SX actually needs from its representation, surveys the alternatives, and shows that every candidate either fails to meet the requirements or converges toward s-expressions under a different name. The conclusion is uncomfortable but unavoidable: for what SX does, there is no alternative."))
(~doc-section :title "The requirements" :id "requirements"
(p :class "text-stone-600"
"SX is not just a templating language. It is a language that serves simultaneously as:")
(ul :class "space-y-2 text-stone-600"
(li (strong "Markup") " — structure for HTML pages, components, layouts")
(li (strong "Programming language") " — conditionals, iteration, functions, macros, closures")
(li (strong "Wire format") " — what the server sends to the client over HTTP")
(li (strong "Data notation") " — configuration, page definitions, component registries")
(li (strong "Spec language") " — the SX specification is written in SX")
(li (strong "Metaprogramming substrate") " — macros that read, transform, and generate code"))
(p :class "text-stone-600"
"Any replacement must handle all six roles with a single syntax. Not six syntaxes awkwardly interleaved — one. This constraint alone eliminates most candidates, because most representations were designed for one of these roles and are ill-suited to the others.")
(p :class "text-stone-600"
"Beyond versatility, the representation must be:")
(ul :class "space-y-2 text-stone-600"
(li (strong "Homoiconic") " — code must be data and data must be code, because macros and self-hosting require it")
(li (strong "Parseable in one pass") " — no forward references, no context-dependent grammar, because the wire format must be parseable by a minimal client")
(li (strong "Structurally validatable") " — a syntactically valid expression must be checkable without evaluation, because untrusted code from federated nodes must be validated before execution")
(li (strong "Token-efficient") " — minimal syntactic overhead, because the representation travels over the network and is processed by LLMs with finite context windows")
(li (strong "Composable by nesting") " — no special composition mechanisms, because the same operation (putting a list inside a list) must work for markup, logic, and data")))
(~doc-section :title "The candidates" :id "candidates"
(~doc-subsection :title "XML / HTML"
(p :class "text-stone-600"
"The obvious first thought. XML is a tree. HTML is markup. Why not use angle brackets?")
(p :class "text-stone-600"
"XML fails on homoiconicity. The distinction between elements, attributes, text nodes, processing instructions, CDATA sections, entity references, and namespaces means the representation has multiple structural categories that cannot freely substitute for each other. An attribute is not an element. A text node is not a processing instruction. You cannot take an arbitrary XML fragment and use it as code, because XML has no concept of evaluation — it is a serialization format for trees, not a language.")
(p :class "text-stone-600"
"XML also fails on token efficiency. " (code "<div class=\"card\"><h2>Title</h2></div>") " versus " (code "(div :class \"card\" (h2 \"Title\"))") ". The closing tags carry zero information — they are pure redundancy. Over a full application, this redundancy compounds into significantly more bytes on the wire and significantly more tokens in an LLM context window.")
(p :class "text-stone-600"
"XSLT attempted to make XML a programming language. The result is universally regarded as a cautionary tale. Trying to express conditionals and iteration in a format designed for document markup produces something that is bad at both."))
(~doc-subsection :title "JSON"
(p :class "text-stone-600"
"JSON is data notation. It has objects, arrays, strings, numbers, booleans, and null. It parses in one pass. It validates structurally. It is ubiquitous.")
(p :class "text-stone-600"
"JSON is not homoiconic because it has no concept of evaluation. It is " (em "inert") " data. To make JSON a programming language, you must invent a convention for representing code — and every such convention reinvents s-expressions with worse ergonomics:")
(~doc-code :code (highlight ";; JSON \"code\" (actual example from various JSON-based DSLs)\n{\"if\": [{\">\": [\"$.count\", 0]},\n {\"map\": [\"$.items\", {\"fn\": [\"item\", {\"get\": [\"item\", \"name\"]}]}]},\n {\"literal\": \"No items\"}]}\n\n;; The same thing in s-expressions\n(if (> count 0)\n (map (fn (item) (get item \"name\")) items)\n \"No items\")" "lisp"))
(p :class "text-stone-600"
"The JSON version is an s-expression encoded in JSON's syntax — lists-of-lists with a head element that determines semantics. It has strictly more punctuation (colons, commas, braces, brackets, quotes around keys) and strictly less readability. Every JSON-based DSL that reaches sufficient complexity converges on this pattern and then wishes it had just used s-expressions."))
(~doc-subsection :title "YAML"
(p :class "text-stone-600"
"YAML is the other common data notation. It adds indentation sensitivity, anchors, aliases, multi-line strings, type coercion, and a " (a :href "https://yaml.org/spec/1.2.2/" :class "text-violet-600 hover:underline" "specification") " that is 240 pages long. The spec for SX's parser is 200 lines.")
(p :class "text-stone-600"
"Indentation sensitivity is a direct disqualifier for wire formats. Whitespace must survive serialization, transmission, minification, and reconstruction exactly — a fragility that s-expressions do not have. YAML also fails on structural validation: the " (a :href "https://en.wikipedia.org/wiki/Norway_problem" :class "text-violet-600 hover:underline" "Norway problem") " (" (code "NO") " parsed as boolean " (code "false") ") demonstrates that YAML's type coercion makes structural validation impossible without semantic knowledge of the schema.")
(p :class "text-stone-600"
"YAML is not homoiconic. It has no evaluation model. Like JSON, any attempt to encode logic in YAML produces s-expressions with worse syntax."))
(~doc-subsection :title "JSX / Template literals"
(p :class "text-stone-600"
"JSX is the closest mainstream technology to what SX does — it embeds markup in a programming language. But JSX is not a representation; it is a compile target. " (code "<Card title=\"Hi\">content</Card>") " compiles to " (code "React.createElement(Card, {title: \"Hi\"}, \"content\")") ". The angle-bracket syntax is sugar that does not survive to runtime.")
(p :class "text-stone-600"
"This means JSX cannot be a wire format — the client must have the compiler. It cannot be a spec language — you cannot write a JSX spec in JSX without a build step. It cannot be a data notation — it requires JavaScript evaluation context. JSX handles exactly one of the six roles (markup) and delegates the others to JavaScript, CSS, JSON, and whatever build tool assembles them.")
(p :class "text-stone-600"
"Template literals (tagged templates in JavaScript, Jinja, ERB, etc.) are string interpolation. They embed code in strings or strings in code, depending on which layer you consider primary. Neither direction produces a homoiconic representation. You cannot write a macro that reads a template literal and transforms it as data, because the template literal is a string — opaque, uninspectable, and unstructured."))
(~doc-subsection :title "Tcl"
(p :class "text-stone-600"
"Tcl is the most interesting near-miss. \"Everything is a string\" is a radical simplification. The syntax is minimal: commands are words separated by spaces, braces group without substitution, brackets evaluate. Tcl is effectively homoiconic — code is strings, strings are code, and " (code "eval") " is the universal mechanism.")
(p :class "text-stone-600"
"Where Tcl falls short is structural validation. Because everything is a string, you cannot check that a Tcl program is well-formed without evaluating it. Unmatched braces inside string values are indistinguishable from syntax errors without context. S-expressions have a trivial structural check — balanced parentheses — that requires no evaluation and no context. For sandboxed evaluation of untrusted code (federated expressions from other nodes), this difference is decisive.")
(p :class "text-stone-600"
"Tcl also lacks native tree structure. Lists are flat strings that are parsed on demand. Nested structure exists by convention, not by grammar. This makes composition more fragile than s-expressions, where nesting is the fundamental structural primitive."))
(~doc-subsection :title "Rebol / Red"
(p :class "text-stone-600"
"Rebol is the strongest alternative. It is homoiconic — code is data. It has minimal syntax. It has dialecting — the ability to create domain-specific languages within the language. It is a single representation for code, data, and markup. " (a :href "https://en.wikipedia.org/wiki/Rebol" :class "text-violet-600 hover:underline" "Carl Sassenrath") " designed it explicitly to solve the problems that SX also targets.")
(p :class "text-stone-600"
"Rebol's limitation is practical, not theoretical. The language is obscure. Modern AI models have almost no training data for it — generating reliable Rebol would require extensive fine-tuning or few-shot prompting. There is no ecosystem of libraries, no community producing components, no federation protocol. And Rebol's type system (over 40 built-in datatypes including " (code "url!") ", " (code "email!") ", " (code "money!") ") makes the parser substantially more complex than s-expressions, which have essentially one composite type: the list.")
(p :class "text-stone-600"
"Rebol demonstrates that the design space around s-expressions has room for variation. But the variations add complexity without adding expressiveness — and in the current landscape, complexity kills AI compatibility and adoption equally."))
(~doc-subsection :title "Forth / stack-based"
(p :class "text-stone-600"
"Forth has the most minimal syntax imaginable: words separated by spaces. No parentheses, no brackets, no delimiters. The program is a flat sequence of tokens. This is simpler than s-expressions.")
(p :class "text-stone-600"
"But Forth's simplicity is deceptive. The flat token stream encodes tree structure " (em "implicitly") " via stack effects. Understanding what a Forth program does requires mentally simulating the stack — tracking what each word pushes and pops. This is precisely the kind of implicit state tracking that both humans and AI models struggle with. A nested s-expression makes structure " (em "visible") ". A Forth program hides it.")
(p :class "text-stone-600"
"For markup, this is fatal. " (code "3 1 + 4 * 2 /") " is arithmetic. Now imagine a page layout expressed as stack operations. The nesting that makes " (code "(div (h2 \"Title\") (p \"Body\"))") " self-evident becomes an exercise in mental bookkeeping. UI is trees. Stack languages are not.")))
(~doc-section :title "The convergence" :id "convergence"
(p :class "text-stone-600"
"Every alternative either fails to meet the requirements or reinvents s-expressions:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "Candidate")
(th :class "px-3 py-2 font-medium text-stone-600" "Homoiconic")
(th :class "px-3 py-2 font-medium text-stone-600" "Structural validation")
(th :class "px-3 py-2 font-medium text-stone-600" "Token-efficient")
(th :class "px-3 py-2 font-medium text-stone-600" "Tree-native")
(th :class "px-3 py-2 font-medium text-stone-600" "AI-trainable")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "XML/HTML")
(td :class "px-3 py-2 text-red-600" "No")
(td :class "px-3 py-2 text-green-700" "Yes")
(td :class "px-3 py-2 text-red-600" "No")
(td :class "px-3 py-2 text-green-700" "Yes")
(td :class "px-3 py-2 text-green-700" "Yes"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "JSON")
(td :class "px-3 py-2 text-red-600" "No")
(td :class "px-3 py-2 text-green-700" "Yes")
(td :class "px-3 py-2 text-red-600" "No")
(td :class "px-3 py-2 text-green-700" "Yes")
(td :class "px-3 py-2 text-green-700" "Yes"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "YAML")
(td :class "px-3 py-2 text-red-600" "No")
(td :class "px-3 py-2 text-red-600" "No")
(td :class "px-3 py-2 text-amber-600" "Moderate")
(td :class "px-3 py-2 text-red-600" "No")
(td :class "px-3 py-2 text-green-700" "Yes"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "JSX")
(td :class "px-3 py-2 text-red-600" "No")
(td :class "px-3 py-2 text-red-600" "Needs compiler")
(td :class "px-3 py-2 text-amber-600" "Moderate")
(td :class "px-3 py-2 text-green-700" "Yes")
(td :class "px-3 py-2 text-green-700" "Yes"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Tcl")
(td :class "px-3 py-2 text-green-700" "Yes")
(td :class "px-3 py-2 text-red-600" "No")
(td :class "px-3 py-2 text-green-700" "Yes")
(td :class "px-3 py-2 text-red-600" "No")
(td :class "px-3 py-2 text-red-600" "No"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Rebol/Red")
(td :class "px-3 py-2 text-green-700" "Yes")
(td :class "px-3 py-2 text-amber-600" "Complex")
(td :class "px-3 py-2 text-green-700" "Yes")
(td :class "px-3 py-2 text-green-700" "Yes")
(td :class "px-3 py-2 text-red-600" "No"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Forth")
(td :class "px-3 py-2 text-amber-600" "Sort of")
(td :class "px-3 py-2 text-green-700" "Yes")
(td :class "px-3 py-2 text-green-700" "Yes")
(td :class "px-3 py-2 text-red-600" "No")
(td :class "px-3 py-2 text-red-600" "No"))
(tr :class "border-b border-stone-200 bg-violet-50"
(td :class "px-3 py-2 font-semibold text-violet-800" "S-expressions")
(td :class "px-3 py-2 text-green-700 font-semibold" "Yes")
(td :class "px-3 py-2 text-green-700 font-semibold" "Yes")
(td :class "px-3 py-2 text-green-700 font-semibold" "Yes")
(td :class "px-3 py-2 text-green-700 font-semibold" "Yes")
(td :class "px-3 py-2 text-green-700 font-semibold" "Yes")))))
(p :class "text-stone-600"
"No candidate achieves all five properties. The closest — Rebol — fails on AI trainability, which is not a theoretical concern but a practical one: a representation that AI cannot generate reliably is a representation that cannot participate in the coming decade of software development."))
(~doc-section :title "Why not invent something new?" :id "invent"
(p :class "text-stone-600"
"The objection might be: fine, existing alternatives fall short, but why not design a new representation that has all these properties without the parentheses?")
(p :class "text-stone-600"
"Try it. You need a tree structure (for markup and composition). You need a uniform representation (for homoiconicity). You need a delimiter that is unambiguous (for one-pass parsing and structural validation). You need minimum syntactic overhead (for token efficiency).")
(p :class "text-stone-600"
"A tree needs a way to mark where a node begins and ends. The minimal delimiter pair is two characters — one for open, one for close. S-expressions use " (code "(") " and " (code ")") ". You could use " (code "[") " and " (code "]") ", or " (code "{") " and " (code "}") ", or " (code "BEGIN") " and " (code "END") ", or indentation. But " (code "[list]") " and " (code "{list}") " are just s-expressions with different brackets. " (code "BEGIN/END") " adds token overhead. Indentation adds whitespace sensitivity, which breaks wire-format reliability.")
(p :class "text-stone-600"
"You could try eliminating delimiters entirely and using a binary format. But binary formats are not human-readable, not human-writable, and not inspectable in a terminal — which means they cannot serve as a programming language. The developer experience of reading and writing code requires a text-based representation, and the minimal text-based tree delimiter is a matched pair of single characters.")
(p :class "text-stone-600"
"You could try significant whitespace — indentation-based nesting like Python or Haskell. This works for programming languages where the code is stored in files and processed by a single toolchain. It does not work for wire formats, where the representation must survive HTTP transfer, server-side generation, client-side parsing, minification, storage in databases, embedding in script tags, and concatenation with other expressions. Whitespace-sensitive formats are fragile in exactly the contexts where SX operates.")
(p :class "text-stone-600"
"Every path through the design space either arrives at parenthesized prefix notation — s-expressions — or introduces complexity that violates one of the requirements. This is not a failure of imagination. It is a consequence of the requirements being simultaneously demanding and precise. The solution space has one optimum, and McCarthy found it in 1958."))
(~doc-section :title "The parentheses objection" :id "parentheses"
(p :class "text-stone-600"
"The real objection to s-expressions is not technical. It is aesthetic. People do not like parentheses. They look unfamiliar. They feel old. They trigger memories of computer science lectures about recursive descent parsers.")
(p :class "text-stone-600"
"This is a human problem, not a representation problem. And it is a human problem with a known trajectory: every programmer who has used a Lisp for more than a few weeks stops seeing the parentheses. They see the tree. The delimiters become invisible, like the spaces between words in English. You do not see spaces. You see words. Lisp programmers do not see parentheses. They see structure.")
(p :class "text-stone-600"
"More to the point: in the world we are entering, most code will be generated by AI and rendered by machines. The human reads the " (em "output") " — the rendered page, the test results, the behaviour. The s-expressions are an intermediate representation that the human steers but does not need to manually type or visually parse character-by-character. The aesthetic objection dissolves when the representation is a conversation between the human's intent and the machine's generation, not something the human stares at in a text editor.")
(p :class "text-stone-600"
"The author of SX has never opened the codebase in an editor. Every file was created through " (a :href "https://claude.ai/" :class "text-violet-600 hover:underline" "Claude") " in a terminal. The parentheses are between the human and the machine, and neither one minds them."))
(~doc-section :title "The conclusion" :id "conclusion"
(p :class "text-stone-600"
"S-expressions are the minimal tree representation. They are the only widely-known homoiconic notation. They have trivial structural validation, maximum token efficiency, and native composability. They are well-represented in AI training data. Every alternative either fails on one of these criteria or converges toward s-expressions under a different name.")
(p :class "text-stone-600"
"This is not a claim that s-expressions are the best syntax for every programming language. They are not. Python's indentation-based syntax is better for imperative scripting. Haskell's layout rules are better for type-heavy functional programming. SQL is better for relational queries.")
(p :class "text-stone-600"
"The claim is narrower and stronger: for a system that must simultaneously serve as markup, programming language, wire format, data notation, spec language, and metaprogramming substrate — with homoiconicity, one-pass parsing, structural validation, token efficiency, and composability — there is no alternative to s-expressions. Not because alternatives have not been tried. Not because the design space has not been explored. But because the requirements, when stated precisely, admit exactly one family of solutions, and that family is the one McCarthy discovered sixty-seven years ago.")
(p :class "text-stone-600"
"The name for this, borrowed from a " (a :href "https://en.wikipedia.org/wiki/There_is_no_alternative" :class "text-violet-600 hover:underline" "different context entirely") ", is " (em "TINA") " — there is no alternative. Not as a political slogan, but as a mathematical observation. When you need a minimal, homoiconic, structurally-validatable, token-efficient, tree-native, AI-compatible representation for the web, you need s-expressions. Everything else is either less capable or isomorphic."))))

View File

@@ -0,0 +1,2 @@
(defcomp ~essay-on-demand-css ()
(~doc-page :title "On-Demand CSS: Killing the Tailwind Bundle" (~doc-section :title "The problem" :id "problem" (p :class "text-stone-600" "Tailwind CSS generates a utility class for every possible combination. The full CSS file is ~4MB. The purged output for a typical site is 20-50KB. Purging requires a build step that scans your source files for class names. This means: a build tool, a config file, a CI step, and a prayer that the scanner finds all your dynamic classes.")) (~doc-section :title "The sx approach" :id "approach" (p :class "text-stone-600" "sx takes a different path. At server startup, the full Tailwind CSS file is parsed into a dictionary keyed by class name. When rendering a response, sx scans the s-expression source for :class attribute values and looks up only those classes. The result: exact CSS, zero build step.") (p :class "text-stone-600" "Component definitions are pre-scanned at registration time. Page-specific sx is scanned at request time. The union of classes is resolved to CSS rules.")) (~doc-section :title "Incremental delivery" :id "incremental" (p :class "text-stone-600" "After the first page load, the client tracks which CSS classes it already has. On subsequent navigations, it sends a hash of its known classes in the SX-Css header. The server computes the diff and sends only new rules. A typical navigation adds 0-10 new rules — a few hundred bytes at most.")) (~doc-section :title "The tradeoff" :id "tradeoff" (p :class "text-stone-600" "The server holds ~4MB of parsed CSS in memory. Regex scanning is not perfect — dynamically constructed class names will not be found. In practice this rarely matters because sx components use mostly static class strings."))))

View File

@@ -0,0 +1,21 @@
;; Essay content — static content extracted from essays.py
;; ---------------------------------------------------------------------------
;; Philosophy section content
;; ---------------------------------------------------------------------------
(defcomp ~philosophy-index-content ()
(~doc-page :title "Philosophy"
(div :class "space-y-4"
(p :class "text-lg text-stone-600 mb-4"
"The deeper ideas behind SX — manifestos, self-reference, and the philosophical traditions that shaped the language.")
(div :class "space-y-3"
(map (fn (item)
(a :href (get item "href")
:sx-get (get item "href") :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
:class "block rounded border border-stone-200 p-4 hover:border-violet-300 hover:bg-violet-50 transition-colors"
(div :class "font-semibold text-stone-800" (get item "label"))
(when (get item "summary")
(p :class "text-sm text-stone-500 mt-1" (get item "summary")))))
philosophy-nav-items)))))

View File

@@ -0,0 +1,86 @@
;; ---------------------------------------------------------------------------
;; React is Hypermedia
;; ---------------------------------------------------------------------------
(defcomp ~essay-react-is-hypermedia ()
(~doc-page :title "React is Hypermedia"
(p :class "text-stone-500 text-sm italic mb-8"
"A React Island is a hypermedia control. Its behavior is specified in SX.")
(~doc-section :title "I. The argument" :id "argument"
(p :class "text-stone-600"
"React is not hypermedia. Everyone knows this. React is a JavaScript UI library. It renders components to a virtual DOM. It diffs. It patches. It manages state. It does none of the things that define " (a :href "https://en.wikipedia.org/wiki/Hypermedia" :class "text-violet-600 hover:underline" "hypermedia") " — server-driven content, links as the primary interaction mechanism, representations that carry their own controls.")
(p :class "text-stone-600"
"And yet. Consider what a React Island actually is:")
(ul :class "list-disc pl-6 space-y-1 text-stone-600"
(li "It is embedded in a server-rendered page.")
(li "Its initial content is delivered as HTML (or as serialised SX, which the client renders to DOM).")
(li "It occupies a region of the page — a bounded area with a defined boundary.")
(li "It responds to user interaction by mutating its own DOM.")
(li "It does not fetch data. It does not route. It does not manage application state outside its boundary."))
(p :class "text-stone-600"
"This is a " (a :href "https://en.wikipedia.org/wiki/Hypermedia#Controls" :class "text-violet-600 hover:underline" "hypermedia control") ". It is a region of a hypermedia document that responds to user input. Like a " (code "<form>") ". Like an " (code "<a>") ". Like an " (code "<input>") ". The difference is that a form's behavior is specified by the browser and the HTTP protocol. An island's behavior is specified in SX."))
(~doc-section :title "II. What makes something hypermedia" :id "hypermedia"
(p :class "text-stone-600"
"Roy " (a :href "https://en.wikipedia.org/wiki/Roy_Fielding" :class "text-violet-600 hover:underline" "Fielding") "'s " (a :href "https://en.wikipedia.org/wiki/Representational_state_transfer" :class "text-violet-600 hover:underline" "REST") " thesis defines hypermedia by a constraint: " (em "hypermedia as the engine of application state") " (HATEOAS). The server sends representations that include controls — links, forms — and the client's state transitions are driven by those controls. The client does not need out-of-band knowledge of what actions are available. The representation " (em "is") " the interface.")
(p :class "text-stone-600"
"A traditional SPA violates this. The client has its own router, its own state machine, its own API client that knows the server's URL structure. The HTML is a shell; the actual interface is constructed from JavaScript and API calls. The representation is not the interface — the representation is a loading spinner while the real interface builds itself.")
(p :class "text-stone-600"
"An SX page does not violate this. The server sends a complete representation — an s-expression tree — that includes all controls. Some controls are plain HTML: " (code "(a :href \"/about\" :sx-get \"/about\")") ". Some controls are reactive islands: " (code "(defisland counter (let ((count (signal 0))) ...))") ". Both are embedded in the representation. Both are delivered by the server. The client does not decide what controls exist — the server does, by including them in the document.")
(p :class "text-stone-600"
"The island is not separate from the hypermedia. The island " (em "is") " part of the hypermedia. It is a control that the server chose to include, whose behavior the server specified, in the same format as the rest of the page."))
(~doc-section :title "III. The SX specification layer" :id "spec-layer"
(p :class "text-stone-600"
"A " (code "<form>") "'s behavior is specified in HTML + HTTP: " (code "method=\"POST\"") ", " (code "action=\"/submit\"") ". The browser reads the specification and executes it — serialise the inputs, make the request, handle the response. The form does not contain JavaScript. Its behavior is declared.")
(p :class "text-stone-600"
"An SX island's behavior is specified in SX:")
(~doc-code :lang "lisp" :code
"(defisland todo-adder\n (let ((text (signal \"\")))\n (form :on-submit (fn (e)\n (prevent-default e)\n (emit-event \"todo:add\" (deref text))\n (reset! text \"\"))\n (input :type \"text\"\n :bind text\n :placeholder \"What needs doing?\")\n (button :type \"submit\" \"Add\"))))")
(p :class "text-stone-600"
"This is a " (em "declaration") ", not a program. It declares: there is a signal holding text. There is a form. When submitted, it emits an event and resets the signal. There is an input bound to the signal. There is a button.")
(p :class "text-stone-600"
"The s-expression " (em "is") " the specification. It is not compiled to JavaScript and then executed as an opaque blob. It is parsed, evaluated, and rendered by a transparent evaluator whose own semantics are specified in the same format (" (code "eval.sx") "). The island's behavior is as inspectable as a form's " (code "action") " attribute — you can read it, quote it, transform it, analyse it. You can even send it over the wire and have a different client render it.")
(p :class "text-stone-600"
"A form says " (em "what to do") " in HTML attributes. An island says " (em "what to do") " in s-expressions. Both are declarative. Both are part of the hypermedia document. The difference is expressiveness: forms can collect inputs and POST them. Islands can maintain local state, compute derived values, animate transitions, handle errors, and render dynamic lists — all declared in the same markup language as the page that contains them."))
(~doc-section :title "IV. The four levels" :id "four-levels"
(p :class "text-stone-600"
"SX reactive islands exist at four levels of complexity, from pure hypermedia to full client reactivity. Each level is a superset of the one before:")
(ul :class "list-disc pl-6 space-y-2 text-stone-600"
(li (span :class "font-semibold" "L0 — Static server rendering.") " No client interactivity. The server evaluates the full component tree and sends HTML. Pure hypermedia. " (code "(div :class \"card\" (h2 title))") ".")
(li (span :class "font-semibold" "L1 — Hypermedia attributes.") " Server-rendered content with htmx-style attributes. " (code "(button :sx-get \"/items\" :sx-target \"#list\")") ". Still server-driven. The client swaps HTML fragments. Classic hypermedia with AJAX.")
(li (span :class "font-semibold" "L2 — Reactive islands.") " Self-contained client-side state within a server-rendered page. " (code "(defisland counter ...)") ". The island is a hypermedia control: the server delivers it, the client executes it. Signals, computed values, effects — all inside the island boundary.")
(li (span :class "font-semibold" "L3 — Island communication.") " Islands talk to each other and to the htmx-like \"lake\" via DOM events. " (code "(emit-event \"cart:updated\" count)") " and " (code "(on-event \"cart:updated\" handler)") ". Still no global state. Still no client-side routing. The page is still a server document with embedded controls."))
(p :class "text-stone-600"
"At every level, the architecture is hypermedia. The server produces the document. The document contains controls. The controls are specified in SX. The jump from L1 to L2 is not a jump from hypermedia to SPA — it is a jump from " (em "simple controls") " (links and forms) to " (em "richer controls") " (reactive islands). The paradigm does not change. The expressiveness does."))
(~doc-section :title "V. Why not just React?" :id "why-not-react"
(p :class "text-stone-600"
"If an island behaves like a React component — local state, event handlers, conditional rendering — why not use React?")
(p :class "text-stone-600"
"Because React requires a " (em "build") ". JSX must be compiled. Modules must be bundled. The result is an opaque JavaScript blob that the server cannot inspect, the wire format cannot represent, and the client must execute before anything is visible. The component's specification — its source code — is lost by the time it reaches the browser.")
(p :class "text-stone-600"
"An SX island arrives at the browser as source. The same s-expression that defined the island on the server is the s-expression that the client parses and evaluates. There is no compilation, no bundling, no build step. The specification " (em "is") " the artifact.")
(p :class "text-stone-600"
"This matters because hypermedia's core property is " (em "self-description") ". A hypermedia representation carries its own controls and its own semantics. An HTML form is self-describing: the browser reads the " (code "action") " and " (code "method") " and knows what to do. A compiled React component is not self-describing: it is a function that was once source code, compiled away into instructions that only the React runtime can interpret.")
(p :class "text-stone-600"
"SX islands are self-describing. The source is the artifact. The representation carries its own semantics. This is what makes them hypermedia controls — not because they avoid JavaScript (they don't), but because the behavior specification travels with the document, in the same format as the document."))
(~doc-section :title "VI. The bridge pattern" :id "bridge"
(p :class "text-stone-600"
"In practice, the hypermedia and the islands coexist through a pattern: the htmx \"lake\" surrounds the reactive \"islands.\" The lake handles navigation, form submission, content loading — classic hypermedia. The islands handle local interaction — counters, toggles, filters, input validation, animations.")
(p :class "text-stone-600"
"Communication between lake and islands uses DOM events. An island can " (code "emit-event") " to tell the page something happened. A server-rendered button can " (code "bridge-event") " to poke an island when clicked. The DOM — the shared medium — is the only coupling.")
(~doc-code :lang "lisp" :code
";; Server-rendered lake button dispatches to island\n(button :sx-get \"/api/refresh\"\n :sx-target \"#results\"\n :on-click (bridge-event \"search:clear\")\n \"Reset\")\n\n;; Island listens for the event\n(defisland search-filter\n (let ((query (signal \"\")))\n (on-event \"search:clear\" (fn () (reset! query \"\")))\n (input :bind query :placeholder \"Filter...\")))")
(p :class "text-stone-600"
"The lake button does its hypermedia thing — fetches HTML, swaps it in. Simultaneously, it dispatches a DOM event. The island hears the event and clears its state. Neither knows about the other's implementation. They communicate through the hypermedia document's event system — the DOM.")
(p :class "text-stone-600"
"This is not a hybrid architecture bolting two incompatible models together. It is a single model — hypermedia — with controls of varying complexity. Some controls are links. Some are forms. Some are reactive islands. All are specified in the document. All are delivered by the server."))
(~doc-section :title "VII. The specification is the specification" :id "specification"
(p :class "text-stone-600"
"The deepest claim is not architectural but philosophical. A React Island — the kind with signals and effects and computed values — is a " (em "behavior specification") ". It specifies: when this signal changes, recompute this derived value, re-render this DOM subtree. When this event fires, update this state. When this input changes, validate against this pattern.")
(p :class "text-stone-600"
"In React, this specification is written in JavaScript and destroyed by compilation. The specification exists only in the developer's source file. The user receives a bundle.")
(p :class "text-stone-600"
"In SX, this specification is written in s-expressions, transmitted as s-expressions, parsed as s-expressions, and evaluated as s-expressions. The specification exists at every stage of the pipeline. It is never destroyed. It is never transformed into something else. It arrives at the browser intact, readable, inspectable.")
(p :class "text-stone-600"
"And the evaluator that interprets this specification? It is itself specified in s-expressions (" (code "eval.sx") "). And the renderer? Specified in s-expressions (" (code "render.sx") "). And the parser? Specified in s-expressions (" (code "parser.sx") "). The specification language specifies itself. The island's behavior is specified in a language whose behavior is specified in itself.")
(p :class "text-stone-600"
"A React Island is a hypermedia control. Its behavior is specified in SX. And SX is specified in SX. There is no layer beneath. The specification goes all the way down."))))

View File

@@ -0,0 +1,102 @@
(defcomp ~essay-reflexive-web ()
(~doc-page :title "The Reflexive Web"
(p :class "text-stone-500 text-sm italic mb-8"
"What happens when the web can read, modify, and reason about itself — and AI is a native participant.")
(~doc-section :title "The missing property" :id "missing-property"
(p :class "text-stone-600"
"Every web technology stack shares one structural limitation: the system cannot inspect itself. A " (a :href "https://en.wikipedia.org/wiki/React_(software)" :class "text-violet-600 hover:underline" "React") " component tree is opaque at runtime. An " (a :href "https://en.wikipedia.org/wiki/HTML" :class "text-violet-600 hover:underline" "HTML") " page cannot read its own structure and generate a new page from it. A " (a :href "https://en.wikipedia.org/wiki/JavaScript" :class "text-violet-600 hover:underline" "JavaScript") " bundle is compiled, minified, and sealed — the running code bears no resemblance to the source that produced it.")
(p :class "text-stone-600"
"The property these systems lack has a name: " (a :href "https://en.wikipedia.org/wiki/Reflection_(computer_programming)" :class "text-violet-600 hover:underline" "reflexivity") ". A reflexive system can represent itself, reason about its own structure, and modify itself based on that reasoning. " (a :href "https://en.wikipedia.org/wiki/Lisp_(programming_language)" :class "text-violet-600 hover:underline" "Lisp") " has had this property " (a :href "https://en.wikipedia.org/wiki/Lisp_(programming_language)#History" :class "text-violet-600 hover:underline" "since 1958") ". The web has never had it.")
(p :class "text-stone-600"
"SX is a complete Lisp. It has " (a :href "https://en.wikipedia.org/wiki/Homoiconicity" :class "text-violet-600 hover:underline" "homoiconicity") " — code is data, data is code. It has a " (a :href "/specs/core" :class "text-violet-600 hover:underline" "self-hosting specification") " — SX defined in SX. It has " (code "eval") " and " (code "quote") " and " (a :href "https://en.wikipedia.org/wiki/Macro_(computer_science)#Syntactic_macros" :class "text-violet-600 hover:underline" "macros") ". And it runs on the wire — the format that travels between server and client IS the language. This combination has consequences."))
(~doc-section :title "What homoiconicity changes" :id "homoiconicity"
(p :class "text-stone-600"
(code "(defcomp ~card (&key title body) (div :class \"p-4\" (h2 title) (p body)))") " — this is simultaneously a program that renders a card AND a list that can be inspected, transformed, and composed by other programs. The " (code "defcomp") " is not compiled away. It is not transpiled into something else. It persists as data at every stage: definition, transmission, evaluation, and rendering.")
(p :class "text-stone-600"
"This means:")
(ul :class "space-y-2 text-stone-600 mt-2"
(li (strong "The component registry is data.") " You can " (code "(map ...)") " over every component in the system, extract their parameter signatures, find all components that render a " (code "(table ...)") ", or generate documentation automatically — because the source IS the runtime representation.")
(li (strong "Programs can write programs.") " A " (a :href "https://en.wikipedia.org/wiki/Macro_(computer_science)#Syntactic_macros" :class "text-violet-600 hover:underline" "macro") " takes a list and returns a list. The returned list is code. The macro runs at expansion time and produces new components, new page definitions, new routing rules — indistinguishable from hand-written ones.")
(li (strong "The wire format is inspectable.") " What the server sends to the client is not a blob of serialized state. It is s-expressions that any system — browser, AI, another server — can parse, reason about, and act on.")))
(~doc-section :title "AI as a native speaker" :id "ai-native"
(p :class "text-stone-600"
"Current AI integration with the web is mediated through layers of indirection. An " (a :href "https://en.wikipedia.org/wiki/Large_language_model" :class "text-violet-600 hover:underline" "LLM") " generates " (a :href "https://en.wikipedia.org/wiki/React_(software)" :class "text-violet-600 hover:underline" "React") " components as strings that must be compiled, bundled, and deployed. It interacts with APIs through " (a :href "https://en.wikipedia.org/wiki/JSON" :class "text-violet-600 hover:underline" "JSON") " endpoints that require separate documentation. It reads HTML by scraping, because the markup was never meant to be machine-readable in a computational sense.")
(p :class "text-stone-600"
"In an SX web, the AI reads the same s-expressions the browser reads. The component definitions " (em "are") " the documentation — a " (code "defcomp") " declares its parameters, its structure, and its semantics in one expression. There is no " (a :href "https://en.wikipedia.org/wiki/OpenAPI_Specification" :class "text-violet-600 hover:underline" "Swagger spec") " describing an API. The API " (em "is") " the language, and the language is self-describing.")
(p :class "text-stone-600"
"An AI that understands SX understands the " (a :href "/specs/core" :class "text-violet-600 hover:underline" "spec") ". And the spec is written in SX. So the AI understands the definition of the language it is using, in the language it is using. This " (a :href "https://en.wikipedia.org/wiki/Reflexivity_(social_theory)" :class "text-violet-600 hover:underline" "reflexive") " property means the AI does not need a separate mental model for \"the web\" and \"the language\" — they are the same thing."))
(~doc-section :title "Live system modification" :id "live-modification"
(p :class "text-stone-600"
"Because code is data and the wire format is the language, modifying a running system is not deployment — it is evaluation. An AI reads " (code "(defcomp ~checkout-form ...)") ", understands what it does (because the semantics are specified in SX), modifies the expression, and sends it back. The system evaluates the new definition. No build step. No deploy pipeline. No container restart.")
(p :class "text-stone-600"
"This is not theoretical — it is how " (a :href "https://en.wikipedia.org/wiki/Lisp_(programming_language)" :class "text-violet-600 hover:underline" "Lisp") " development has always worked. You modify a function in the running image. The change takes effect immediately. What is new is putting this on the wire, across a network, with the AI as a participant rather than a tool.")
(p :class "text-stone-600"
"The implications for development itself are significant. An AI does not need to " (em "generate code") " that a human then reviews, commits, builds, and deploys. It can propose a modified expression, the human evaluates it in a sandbox, and if it works, the expression becomes the new definition. The feedback loop shrinks from hours to seconds.")
(p :class "text-stone-600"
"More radically: the distinction between \"development\" and \"operation\" dissolves. If the running system is a set of s-expressions, and those expressions can be inspected and modified at runtime, then there is no separate development environment. There is just the system, and agents — human or artificial — that interact with it."))
(~doc-section :title "Federated intelligence" :id "federated-intelligence"
(p :class "text-stone-600"
(a :href "https://en.wikipedia.org/wiki/ActivityPub" :class "text-violet-600 hover:underline" "ActivityPub") " carries activities between nodes. If those activities contain s-expressions, then what travels between servers is not just data — it is " (em "behaviour") ". Node A sends a component definition to Node B. Node B evaluates it. The result is rendered. The sender's intent is executable on the receiver's hardware.")
(p :class "text-stone-600"
"This is fundamentally different from sending " (a :href "https://en.wikipedia.org/wiki/JSON" :class "text-violet-600 hover:underline" "JSON") " payloads. JSON says \"here is some data, figure out what to do with it.\" An s-expression says \"here is what to do, and here is the data to do it with.\" The component definition and the data it operates on travel together.")
(p :class "text-stone-600"
"For AI agents in a federated network, this means an agent on one node can send " (em "capabilities") " to another node, not just requests. A component that renders a specific visualization. A macro that transforms data into a particular format. A function that implements a protocol. The network becomes a shared computational substrate where intelligence is distributed as executable expressions."))
(~doc-section :title "Programs writing programs writing programs" :id "meta-programs"
(p :class "text-stone-600"
"A macro is a function that takes code and returns code. An AI generating macros is writing programs that write programs. With " (code "eval") ", those generated programs can generate more programs at runtime. This is not a metaphor — it is the literal mechanism.")
(p :class "text-stone-600"
"The " (a :href "/philosophy/godel-escher-bach" :class "text-violet-600 hover:underline" "Gödel numbering") " parallel is not incidental. " (a :href "https://en.wikipedia.org/wiki/Kurt_G%C3%B6del" :class "text-violet-600 hover:underline" "Gödel") " showed that any sufficiently powerful formal system can encode statements about itself. A complete Lisp on the wire is a sufficiently powerful formal system. The web can make statements about itself — components that inspect other components, macros that rewrite the page structure, expressions that generate new expressions based on the current state of the system.")
(p :class "text-stone-600"
"Consider what this enables for AI:")
(ul :class "space-y-2 text-stone-600 mt-2"
(li (strong "Self-improving interfaces.") " An AI observes how users interact with a component (click patterns, error rates, abandonment). It reads the component definition — because it is data. It modifies the definition — because data is code. It evaluates the result. The interface adapts without human intervention.")
(li (strong "Generative composition.") " Given a data schema and a design intent, an AI generates not just a component but the " (em "macros") " that generate families of components. The macro is a template for templates. The output scales combinatorially.")
(li (strong "Cross-system reasoning.") " An AI reads component definitions from multiple federated nodes, identifies common patterns, and synthesizes abstractions that work across all of them. The shared language makes cross-system analysis trivial — it is all s-expressions.")))
(~doc-section :title "The sandbox is everything" :id "sandbox"
(p :class "text-stone-600"
"The same " (a :href "https://en.wikipedia.org/wiki/Homoiconicity" :class "text-violet-600 hover:underline" "homoiconicity") " that makes this powerful makes it dangerous. Code-as-data means an AI can inject " (em "behaviour") ", not just content. A malicious expression evaluated in the wrong context could exfiltrate data, modify other components, or disrupt the system.")
(p :class "text-stone-600"
"This is why the " (a :href "/specs/primitives" :class "text-violet-600 hover:underline" "primitive set") " is the critical security boundary. The spec defines exactly which operations are available. A sandboxed evaluator that only exposes pure primitives (arithmetic, string operations, list manipulation) cannot perform I/O. Cannot access the network. Cannot modify the DOM outside its designated target. The language is " (a :href "https://en.wikipedia.org/wiki/Turing_completeness" :class "text-violet-600 hover:underline" "Turing-complete") " within the sandbox and powerless outside it.")
(p :class "text-stone-600"
"Different contexts grant different primitive sets. A component evaluated in a page slot gets rendering primitives. A macro gets code-transformation primitives. A federated expression from an untrusted node gets the minimal safe set. The sandbox is not bolted on — it is inherent in the language's architecture. What you can do depends on which primitives are in scope.")
(p :class "text-stone-600"
"This matters enormously for AI. An AI agent that can modify the running system must be constrained by the same sandbox mechanism that constrains any other expression. The security model does not distinguish between human-authored code and AI-generated code — both are s-expressions, both are evaluated by the same evaluator, both are subject to the same primitive restrictions."))
(~doc-section :title "Not self-aware — reflexive" :id "reflexive"
(p :class "text-stone-600"
"Is this a \"self-aware web\"? Probably not in the " (a :href "https://en.wikipedia.org/wiki/Consciousness" :class "text-violet-600 hover:underline" "consciousness") " sense. But the word we keep reaching for has a precise meaning: " (a :href "https://en.wikipedia.org/wiki/Reflexivity_(social_theory)" :class "text-violet-600 hover:underline" "reflexivity") ". A reflexive system can represent itself, reason about its own structure, and modify itself based on that reasoning.")
(p :class "text-stone-600"
"A " (a :href "https://en.wikipedia.org/wiki/React_(software)" :class "text-violet-600 hover:underline" "React") " app cannot read its own component tree as data and rewrite it. An HTML page cannot inspect its own structure and generate new pages. A JSON API cannot describe its own semantics in a way that is both human-readable and machine-executable.")
(p :class "text-stone-600"
"SX can do all of these things — because there is no distinction between the program and its representation. The source code, the wire format, the runtime state, and the data model are all the same thing: " (a :href "https://en.wikipedia.org/wiki/S-expression" :class "text-violet-600 hover:underline" "s-expressions") ".")
(p :class "text-stone-600"
"What AI adds to this is not awareness but " (em "agency") ". The system has always been reflexive — Lisp has been reflexive for seven decades. What is new is having an agent that can exploit that reflexivity at scale: reading the entire system state as data, reasoning about it, generating modifications, and evaluating the results — all in the native language of the system itself."))
(~doc-section :title "The Lisp that escaped the REPL" :id "escaped-repl"
(p :class "text-stone-600"
(a :href "https://en.wikipedia.org/wiki/Lisp_(programming_language)" :class "text-violet-600 hover:underline" "Lisp") " has been reflexive since " (a :href "https://en.wikipedia.org/wiki/John_McCarthy_(computer_scientist)" :class "text-violet-600 hover:underline" "McCarthy") ". What kept it contained was the boundary of the " (a :href "https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop" :class "text-violet-600 hover:underline" "REPL") " — a single process, a single machine, a single user. The s-expressions lived inside Emacs, inside a Clojure JVM, inside a Scheme interpreter. They did not travel.")
(p :class "text-stone-600"
"SX puts s-expressions on the wire. Between server and client. Between federated nodes. Between human and AI. The reflexive property escapes the process boundary and becomes a property of the " (em "network") ".")
(p :class "text-stone-600"
"A network of nodes that share a reflexive language is a qualitatively different system from a network of nodes that exchange inert data. The former can reason about itself, modify itself, and evolve. The latter can only shuttle payloads.")
(p :class "text-stone-600"
"Whether this constitutes anything approaching awareness is a philosophical question. What is not philosophical is the engineering consequence: a web built on s-expressions is a web that AI can participate in as a " (em "native citizen") ", not as a tool bolted onto the side. The language is the interface. The interface is the language. And the language can describe itself."))
(~doc-section :title "What this opens up" :id "possibilities"
(p :class "text-stone-600"
"Concretely:")
(ul :class "space-y-3 text-stone-600 mt-2"
(li (strong "AI-native development environments.") " The IDE is a web page. The web page is s-expressions. The AI reads and writes s-expressions. There is no translation layer between what the AI thinks and what the system executes. " (a :href "https://en.wikipedia.org/wiki/Pair_programming" :class "text-violet-600 hover:underline" "Pair programming") " with an AI becomes pair evaluation.")
(li (strong "Adaptive interfaces.") " Components that observe their own usage patterns and propose modifications. The AI reads the component (data), the interaction logs (data), and generates a modified component (data). Human approves or rejects. The loop is native to the system.")
(li (strong "Semantic federation.") " Nodes exchange not just content but " (em "understanding") ". A component definition carries its own semantics. An AI on a receiving node can reason about what a foreign component does without documentation, because the definition is self-describing.")
(li (strong "Emergent protocols.") " Two AI agents on different nodes, speaking SX, can negotiate new interaction patterns by exchanging macros. The protocol is not predefined — it emerges from the conversation between agents, expressed in the shared language.")
(li (strong "Composable trust.") " The sandbox mechanism means you can give an AI agent " (em "exactly") " the capabilities it needs — no more. Trust is expressed as a set of available primitives, not as an all-or-nothing API key."))
(p :class "text-stone-600"
"None of these require breakthroughs in AI. They require a web that speaks a reflexive language. " (a :href "https://en.wikipedia.org/wiki/Lisp_(programming_language)" :class "text-violet-600 hover:underline" "Lisp") " solved the language problem in 1958. SX solves the distribution problem. AI provides the agency. The three together produce something that none of them achieves alone: a web that can reason about itself."))))

View File

@@ -0,0 +1,103 @@
(defcomp ~essay-s-existentialism ()
(~doc-page :title "S-Existentialism"
(p :class "text-stone-500 text-sm italic mb-8"
"Existence precedes essence — and s-expressions exist before anything gives them meaning.")
(~doc-section :title "I. Existence precedes essence" :id "existence-precedes-essence"
(p :class "text-stone-600"
"In 1946, Jean-Paul " (a :href "https://en.wikipedia.org/wiki/Jean-Paul_Sartre" :class "text-violet-600 hover:underline" "Sartre") " gave a lecture called \"" (a :href "https://en.wikipedia.org/wiki/Existentialism_Is_a_Humanism" :class "text-violet-600 hover:underline" "Existentialism Is a Humanism") ".\" Its central claim: " (em "existence precedes essence") ". A paper knife is designed before it exists — someone conceives its purpose, then builds it. A human being is the opposite — we exist first, then define ourselves through our choices. There is no blueprint. There is no human nature that precedes the individual human.")
(p :class "text-stone-600"
"A React component is a paper knife. Its essence precedes its existence. Before a single line of JSX runs, React has decided what a component is: a function that returns elements, governed by the rules of hooks, reconciled by a virtual DOM, managed by a scheduler. The framework defines the essence. The developer fills in the blanks. You exist within React's concept of you.")
(p :class "text-stone-600"
"An s-expression exists before any essence is assigned to it. " (code "(div :class \"card\" (h2 title))") " is a list. That is all it is. It has no inherent meaning. It is not a component, not a template, not a function call — not yet. It is raw existence: a nested structure of symbols, keywords, and other lists, waiting.")
(p :class "text-stone-600"
"The evaluator gives it essence. " (code "render-to-html") " makes it HTML. " (code "render-to-dom") " makes it DOM nodes. " (code "aser") " makes it wire format. " (code "quote") " keeps it as data. The same expression, the same existence, can receive different essences depending on what acts on it. The expression does not know what it is. It becomes what it is used for.")
(~doc-code :lang "lisp" :code
";; The same existence, different essences:\n(define expr '(div :class \"card\" (h2 \"Hello\")))\n\n(render-to-html expr) ;; → <div class=\"card\"><h2>Hello</h2></div>\n(render-to-dom expr) ;; → [DOM Element]\n(aser expr) ;; → (div :class \"card\" (h2 \"Hello\"))\n(length expr) ;; → 4 (it's just a list)\n\n;; The expression existed before any of these.\n;; It has no essence until you give it one."))
(~doc-section :title "II. Condemned to be free" :id "condemned"
(p :class "text-stone-600"
"\"Man is condemned to be free,\" Sartre wrote in " (a :href "https://en.wikipedia.org/wiki/Being_and_Nothingness" :class "text-violet-600 hover:underline" "Being and Nothingness") ". Not free as a gift. Free as a sentence. You did not choose to be free. You cannot escape it. Every attempt to deny your freedom — by deferring to authority, convention, or nature — is " (a :href "https://en.wikipedia.org/wiki/Bad_faith_(existentialism)" :class "text-violet-600 hover:underline" "bad faith") ". You are responsible for everything you make of yourself, and the weight of that responsibility is the human condition.")
(p :class "text-stone-600"
"SX condemns you to be free. There is no framework telling you how to structure your application. No router mandating your URL patterns. No state management library imposing its model. No convention-over-configuration file tree that decides where your code goes. You have a parser, an evaluator, and fifty primitives. What you build is your responsibility.")
(p :class "text-stone-600"
"React developers are not free. They are told: components must be pure. State changes must go through hooks. Side effects must live in useEffect. The render cycle must not be interrupted. These are the commandments. Obey them and the framework rewards you with a working application. Disobey them and the framework punishes you with cryptic errors. This is not freedom. This is " (a :href "https://en.wikipedia.org/wiki/Fear_and_Trembling" :class "text-violet-600 hover:underline" "Kierkegaard's") " knight of faith, submitting to the absurd authority of the framework because the alternative — thinking for yourself — is terrifying.")
(p :class "text-stone-600"
"The SX developer has no commandments. " (code "defcomp") " is a suggestion, not a requirement — you can build components with raw lambdas if you prefer. " (code "defmacro") " gives you the power to reshape the language itself. There are no rules of hooks because there are no hooks. There are no lifecycle methods because there is no lifecycle. There is only evaluation: an expression goes in, a value comes out. What the expression contains, how the value is used — that is up to you.")
(p :class "text-stone-600"
"This is not comfortable. Freedom never is. Sartre did not say freedom was pleasant. He said it was inescapable."))
(~doc-section :title "III. Bad faith" :id "bad-faith"
(p :class "text-stone-600"
(a :href "https://en.wikipedia.org/wiki/Bad_faith_(existentialism)" :class "text-violet-600 hover:underline" "Bad faith") " is Sartre's term for the lie you tell yourself to escape freedom. The waiter who plays at being a waiter — performing the role so thoroughly that he forgets he chose it. The woman who pretends not to notice a man's intentions — denying her own awareness to avoid making a decision. Bad faith is not deception of others. It is self-deception about one's own freedom.")
(p :class "text-stone-600"
"\"We have to use React — it's the industry standard.\" Bad faith. You chose React. The industry did not force it on you. There were alternatives. You preferred the comfort of the herd.")
(p :class "text-stone-600"
"\"We need TypeScript — you can't write reliable code without a type system.\" Bad faith. " (a :href "https://en.wikipedia.org/wiki/Lisp_(programming_language)" :class "text-violet-600 hover:underline" "Lisp") " has been writing reliable code without static types since 1958. " (a :href "https://en.wikipedia.org/wiki/Erlang_(programming_language)" :class "text-violet-600 hover:underline" "Erlang") " runs telephone networks on dynamic types. You chose TypeScript because you are afraid of your own code, and the type system is a security blanket.")
(p :class "text-stone-600"
"\"We need a build step — modern web development requires it.\" Bad faith. A " (code "<script>") " tag requires no build step. An s-expression evaluator in 3,000 lines of JavaScript requires no build step. You need a build step because you chose tools that require a build step, and now you have forgotten that the choice was yours.")
(p :class "text-stone-600"
"\"Nobody uses s-expressions for web development.\" Bad faith. " (em "You") " do not use s-expressions for web development. That is a fact about you, not a fact about web development. Transforming your personal preference into a universal law is the quintessential act of bad faith.")
(p :class "text-stone-600"
"SX does not prevent bad faith — nothing can. But it makes bad faith harder. When the entire language is fifty primitives and a page of special forms, you cannot pretend that the complexity is necessary. When there is no build step, you cannot pretend that the build step is inevitable. When the same source runs on server and client, you cannot pretend that the server-client divide is ontological. SX strips away the excuses. What remains is your choices."))
(~doc-section :title "IV. Nausea" :id "nausea"
(p :class "text-stone-600"
"In " (a :href "https://en.wikipedia.org/wiki/Nausea_(novel)" :class "text-violet-600 hover:underline" "Nausea") " (1938), Sartre's Roquentin sits in a park and stares at the root of a chestnut tree. He sees it — really sees it — stripped of all the concepts and categories that normally make it comprehensible. It is not a \"root.\" It is not \"brown.\" It is not \"gnarled.\" It simply " (em "is") " — a brute, opaque, superfluous existence. The nausea is the vertigo of confronting existence without essence.")
(p :class "text-stone-600"
"Open " (code "node_modules") ". Stare at it. 47,000 directories. 1.2 gigabytes. For a to-do app. Each directory contains a " (code "package.json") ", a " (code "README.md") ", a " (code "LICENSE") ", and some JavaScript that wraps some other JavaScript that wraps some other JavaScript. There is no reason for any of it. It is not necessary. It is not justified. It simply accumulated — dependency after dependency, version after version, a brute, opaque, superfluous existence. This is " (code "node_modules") " nausea.")
(p :class "text-stone-600"
"SX has its own nausea. Stare at a page of s-expressions long enough and the same vertigo hits. Parentheses. Symbols. Lists inside lists inside lists. There is nothing behind them — no hidden runtime, no compiled intermediate form, no framework magic. Just parentheses. The s-expression is Roquentin's chestnut root: it simply " (em "is") ". You cannot unsee it.")
(p :class "text-stone-600"
"But SX's nausea is honest. The chestnut root is really there — it exists, bare and exposed. The " (code "node_modules") " nausea is different: it is nausea at something that should not exist, that has no reason to exist, that exists only because of accumulated accidents of dependency resolution. SX's nausea is existential — the dizziness of confronting raw structure. The JavaScript ecosystem's nausea is absurd — the dizziness of confronting unnecessary complexity that no one chose but everyone maintains."))
(~doc-section :title "V. The absurd" :id "absurd"
(p :class "text-stone-600"
(a :href "https://en.wikipedia.org/wiki/Albert_Camus" :class "text-violet-600 hover:underline" "Camus") " defined the " (a :href "https://en.wikipedia.org/wiki/Absurdism" :class "text-violet-600 hover:underline" "absurd") " as the gap between human longing for meaning and the universe's silence. We want the world to make sense. It does not. The absurd is not in us or in the world — it is in the confrontation between the two.")
(p :class "text-stone-600"
"Web development is absurd. We want simple, composable, maintainable software. The industry gives us webpack configurations, framework migrations, and breaking changes in minor versions. We want to write code and run it. The industry gives us transpilers, bundlers, minifiers, tree-shakers, and hot-module-replacers. The gap between what we want and what we get is the absurd.")
(p :class "text-stone-600"
"Writing a Lisp for the web is also absurd — but in a different register. Nobody asked for it. Nobody wants it. The parentheses are off-putting. The ecosystem is nonexistent. The job market is zero. There is no rational justification for building SX when React exists, when Vue exists, when Svelte exists, when the entire weight of the industry points in the other direction.")
(p :class "text-stone-600"
"Camus said there are three responses to the absurd. " (a :href "https://en.wikipedia.org/wiki/The_Myth_of_Sisyphus" :class "text-violet-600 hover:underline" "Suicide") " — giving up. " (a :href "https://en.wikipedia.org/wiki/Leap_of_faith" :class "text-violet-600 hover:underline" "Philosophical suicide") " — leaping into faith, pretending the absurd has been resolved. Or " (em "revolt") " — continuing without resolution, fully aware that the project is meaningless, and doing it anyway.")
(p :class "text-stone-600"
"Most developers commit philosophical suicide. They adopt a framework, declare it The Way, and stop questioning. React is the truth. TypeScript is salvation. The build step is destiny. The absurd disappears — not because it has been resolved, but because they have stopped looking at it.")
(p :class "text-stone-600"
"SX is revolt. It does not resolve the absurd. It does not pretend that s-expressions are the answer, that parentheses will save the web, that the industry will come around. It simply continues — writing components, specifying evaluators, bootstrapping to new targets — with full awareness that the project may never matter to anyone. This is the only honest response to the absurd."))
(~doc-section :title "VI. Sisyphus" :id "sisyphus"
(p :class "text-stone-600"
"\"" (a :href "https://en.wikipedia.org/wiki/The_Myth_of_Sisyphus" :class "text-violet-600 hover:underline" "One must imagine Sisyphus happy") ".\"")
(p :class "text-stone-600"
"Sisyphus pushes a boulder up a hill. It rolls back down. He pushes it up again. Forever. Camus argues that Sisyphus is the absurd hero: he knows the task is pointless, he does it anyway, and in the doing — in the conscious confrontation with futility — he finds something that transcends the futility.")
(p :class "text-stone-600"
"The framework developer is Sisyphus too, but an unconscious one. React 16 to 17 to 18. Class components to hooks to server components. Each migration is the boulder. Each major version is the hill. The developer pushes the codebase up, and the next release rolls it back down. But the framework developer does not " (em "know") " they are Sisyphus. They believe each migration is progress. They believe the boulder will stay at the top this time. This is philosophical suicide — the leap of faith that the next version will be the last.")
(p :class "text-stone-600"
"The SX developer is conscious Sisyphus. The boulder is obvious: writing a Lisp for the web is absurd. The hill is obvious: nobody will use it. But consciousness changes everything. Camus's Sisyphus is happy not because the task has meaning but because " (em "he") " has chosen it. The choice — the revolt — is the meaning. Not the outcome.")
(p :class "text-stone-600"
"One must imagine the s-expressionist happy."))
(~doc-section :title "VII. Thrownness" :id "thrownness"
(p :class "text-stone-600"
(a :href "https://en.wikipedia.org/wiki/Martin_Heidegger" :class "text-violet-600 hover:underline" "Heidegger's") " " (a :href "https://en.wikipedia.org/wiki/Thrownness" :class "text-violet-600 hover:underline" "Geworfenheit") " — thrownness — describes the condition of finding yourself already in a world you did not choose. You did not pick your language, your culture, your body, your historical moment. You were " (em "thrown") " into them. Authenticity is not escaping thrownness but owning it — relating to your situation as yours, rather than pretending it was inevitable or that you could have been elsewhere.")
(p :class "text-stone-600"
"SX is thrown into the web. It did not choose HTTP, the DOM, CSS, JavaScript engines, or browser security models. These are the givens — the facticity of web development. Every web technology is thrown into this same world. The question is how you relate to it.")
(p :class "text-stone-600"
"React relates to the DOM by replacing it — the virtual DOM is a denial of thrownness, an attempt to build a world that is not the one you were thrown into, then reconcile the two. Angular relates to JavaScript by replacing it — TypeScript, decorators, dependency injection, a whole parallel universe layered over the given one. These are inauthentic responses to thrownness: instead of owning the situation, they construct an alternative and pretend it is the real one.")
(p :class "text-stone-600"
"SX owns its thrownness. It runs in the browser's JavaScript engine — not because JavaScript is good, but because the browser is the world it was thrown into. It produces DOM nodes — not because the DOM is elegant, but because the DOM is what exists. It sends HTTP responses — not because HTTP is ideal, but because HTTP is the wire. SX does not build a virtual DOM to escape the real DOM. It does not invent a type system to escape JavaScript's types. It evaluates s-expressions in the given environment and produces what the environment requires.")
(p :class "text-stone-600"
"The s-expression is itself a kind of primordial thrownness. It did not choose to be the minimal recursive data structure. It simply is. Open paren, atoms, close paren. It was not designed by committee, not optimised by industry, not evolved through market pressure. It was " (a :href "https://en.wikipedia.org/wiki/Lisp_(programming_language)#History" :class "text-violet-600 hover:underline" "discovered in 1958") " as a notational convenience and turned out to be the bedrock. SX was thrown into s-expressions the way humans are thrown into bodies — not by choice, but by the nature of what it is."))
(~doc-section :title "VIII. The Other" :id "the-other"
(p :class "text-stone-600"
"Sartre's account of " (a :href "https://en.wikipedia.org/wiki/Being_and_Nothingness#The_Other_and_the_Look" :class "text-violet-600 hover:underline" "the Other") " in Being and Nothingness: I am alone in a park. I am the centre of my world. Then I see another person. Suddenly I am seen. I am no longer just a subject — I am an object in someone else's world. The Other's gaze transforms me. \"Hell is other people,\" Sartre wrote in " (a :href "https://en.wikipedia.org/wiki/No_Exit" :class "text-violet-600 hover:underline" "No Exit") " — not because others are cruel, but because they see you, and their seeing limits your freedom to define yourself.")
(p :class "text-stone-600"
"Every framework exists under the gaze of the Other. React watches what Vue does. Vue watches what Svelte does. Svelte watches what Solid does. Each framework defines itself partly through the Other — \"we are not React,\" \"we are faster than Vue,\" \"we are simpler than Angular.\" The benchmark is the Other. The identity is relational. No framework is free to be purely itself, because the Others are always watching.")
(p :class "text-stone-600"
"SX has no Other. There is no competing s-expression web framework to define itself against. There is no benchmark to win, no market to capture, no conference talk to rebut. This is either pathetic (no ecosystem, no community, no relevance) or liberating (no gaze, no comparison, no borrowed identity). Sartre would say it is both.")
(p :class "text-stone-600"
"But there is another sense of the Other that matters more. The Other " (em "evaluator") ". SX's self-hosting spec means that the language encounters itself as Other. " (code "eval.sx") " is written in SX — the language looking at itself, seeing itself from outside. The bootstrap compiler reads this self-description and produces a working evaluator. The language has been seen by its own gaze, and the seeing has made it real. This is Sartre's intersubjectivity turned reflexive: the subject and the Other are the same entity."))
(~doc-section :title "IX. Authenticity" :id "authenticity"
(p :class "text-stone-600"
"For both Heidegger and Sartre, " (a :href "https://en.wikipedia.org/wiki/Authenticity_(philosophy)" :class "text-violet-600 hover:underline" "authenticity") " means facing your situation — your freedom, your thrownness, your mortality — without evasion. The inauthentic person hides in the crowd, adopts the crowd's values, speaks the crowd's language. Heidegger called this " (a :href "https://en.wikipedia.org/wiki/Heideggerian_terminology#Das_Man" :class "text-violet-600 hover:underline" "das Man") " — the \"They.\" \"They say React is best.\" \"They use TypeScript.\" \"They have build steps.\" The They is not a conspiracy. It is the comfortable anonymity of doing what everyone does.")
(p :class "text-stone-600"
"Authenticity in web development would mean confronting what you are actually doing: arranging symbols so that a machine produces visual output. That is all web development is. Not \"building products.\" Not \"crafting experiences.\" Not \"shipping value.\" Arranging symbols. The frameworks, the methodologies, the Agile ceremonies — all of it is das Man, the They, the comfortable obfuscation of a simple truth.")
(p :class "text-stone-600"
"SX is more authentic than most — not because s-expressions are morally superior, but because they are harder to hide behind. There is no CLI that generates boilerplate. No convention that tells you where files go. No community consensus on the Right Way. You write expressions. You evaluate them. You see what they produce. The gap between what you do and what happens is as small as it can be.")
(p :class "text-stone-600"
"De Beauvoir added something Sartre did not: authenticity requires that you " (a :href "https://en.wikipedia.org/wiki/The_Ethics_of_Ambiguity" :class "text-violet-600 hover:underline" "will the freedom of others") ", not just your own. A language that locks you into one runtime, one vendor, one ecosystem is inauthentic — it denies others the freedom it claims for itself. SX's self-hosting spec is an act of de Beauvoirian ethics: by defining the language in itself, in a format that any reader can parse, any compiler can target, any host can implement, it wills the freedom of every future evaluator. The spec is public. The language is portable. Your freedom to re-implement, to fork, to understand — that freedom is not a side effect. It is the point.")
(p :class "text-stone-600"
"Existence precedes essence. The s-expression exists — bare, parenthesised, indifferent — before any evaluator gives it meaning. What it becomes is up to you. This is not a limitation. It is the only freedom there is."))))

View File

@@ -0,0 +1,93 @@
(defcomp ~essay-separation-of-concerns ()
(~doc-page :title "Separate your Own Concerns"
(p :class "text-stone-500 text-sm italic mb-8"
"The web's canonical separation — HTML, CSS, JavaScript — separates the framework's concerns, not yours. Real separation of concerns is domain-specific and cannot be prescribed by a platform.")
(~doc-section :title "The orthodoxy" :id "orthodoxy"
(p :class "text-stone-600"
"Web development has an article of faith: separate your concerns. Put structure in HTML. Put presentation in CSS. Put behavior in JavaScript. Three languages, three files, three concerns. This is presented as a universal engineering principle — the web platform's gift to good architecture.")
(p :class "text-stone-600"
"It is nothing of the sort. It is the " (em "framework's") " separation of concerns, not the " (em "application's") ". The web platform needs an HTML parser, a CSS engine, and a JavaScript runtime. These are implementation boundaries internal to the browser. Elevating them to an architectural principle for application developers is like telling a novelist to keep their nouns in one file, verbs in another, and adjectives in a third — because that's how the compiler organises its grammar."))
(~doc-section :title "What is a concern?" :id "what-is-a-concern"
(p :class "text-stone-600"
"A concern is a cohesive unit of functionality that can change independently. In a shopping application, concerns might be: the product card, the cart, the checkout flow, the search bar. Each of these has structure, style, and behavior that change together. When you redesign the product card, you change its markup, its CSS, and its click handlers — simultaneously, for the same reason, in response to the same requirement.")
(p :class "text-stone-600"
"The traditional web separation scatters this single concern across three files. The product card's markup is in " (code :class "text-violet-700" "products.html") ", tangled with every other page element. Its styles are in " (code :class "text-violet-700" "styles.css") ", mixed with hundreds of unrelated rules. Its behavior is in " (code :class "text-violet-700" "app.js") ", coupled to every other handler by shared scope. To change the product card, you edit three files, grep for the right selectors, hope nothing else depends on the same class names, and pray.")
(p :class "text-stone-600"
"This is not separation of concerns. It is " (strong "commingling") " of concerns, organized by language rather than by meaning."))
(~doc-section :title "The framework's concerns are not yours" :id "framework-concerns"
(p :class "text-stone-600"
"The browser has good reasons to separate HTML, CSS, and JavaScript. The HTML parser builds a DOM tree. The CSS engine resolves styles and computes layout. The JS runtime manages execution contexts, event loops, and garbage collection. These are distinct subsystems with distinct performance characteristics, security models, and parsing strategies.")
(p :class "text-stone-600"
"But you are not building a browser. You are building an application. Your concerns are: what does a product card look like? What happens when a user clicks 'add to cart'? How does the search filter update the results? These questions cut across markup, style, and behavior. They are not aligned with the browser's internal module boundaries.")
(p :class "text-stone-600"
"When a framework tells you to separate by technology — HTML here, CSS there, JS over there — it is asking you to organize your application around " (em "its") " architecture, not around your problem domain. You are serving the framework's interests. The framework is not serving yours."))
(~doc-section :title "React understood the problem" :id "react"
(p :class "text-stone-600"
"React's most radical insight was not the virtual DOM or one-way data flow. It was the assertion that a component — markup, style, behavior, all co-located — is the right unit of abstraction for UI. JSX was controversial precisely because it violated the orthodoxy. You are putting HTML in your JavaScript! The concerns are not separated!")
(p :class "text-stone-600"
"But the concerns " (em "were") " separated — by component, not by language. A " (code :class "text-violet-700" "<ProductCard>") " contains everything about product cards. A " (code :class "text-violet-700" "<SearchBar>") " contains everything about search bars. Changing one component does not require changes to another. That is separation of concerns — real separation, based on what changes together.")
(p :class "text-stone-600"
"CSS-in-JS libraries followed the same logic. If styles belong to a component, they should live with that component. Not in a global stylesheet where any selector can collide with any other. The backlash — \"you're mixing concerns!\" — betrayed a fundamental confusion between " (em "technologies") " and " (em "concerns") "."))
(~doc-section :title "Separation of concerns is domain-specific" :id "domain-specific"
(p :class "text-stone-600"
"Here is the key point: " (strong "no framework can tell you what your concerns are") ". Concerns are determined by your domain, your requirements, and your rate of change. A medical records system has different concerns from a social media feed. An e-commerce checkout has different concerns from a real-time dashboard. The boundaries between concerns are discovered through building the application, not prescribed in advance by a platform specification.")
(p :class "text-stone-600"
"A framework that imposes a fixed separation — this file for structure, that file for style — is claiming universal knowledge of every possible application domain. That claim is obviously false. Yet it has shaped twenty-five years of web development tooling, project structures, and hiring practices.")
(p :class "text-stone-600"
"The right question is never \"are your HTML, CSS, and JS in separate files?\" The right question is: \"when a requirement changes, how many files do you touch, and how many of those changes are unrelated to each other?\" If you touch three files and all three changes serve the same requirement, your concerns are not separated — they are scattered."))
(~doc-section :title "What SX does differently" :id "sx-approach"
(p :class "text-stone-600"
"An SX component is a single expression that contains its structure, its style (as keyword-resolved CSS classes), and its behavior (event bindings, conditionals, data flow). Nothing is in a separate file unless it genuinely represents a separate concern.")
(~doc-code :code "(defcomp ~product-card (&key product on-add)
(div :class \"rounded-lg border border-stone-200 p-4 hover:shadow-md transition-shadow\"
(img :src (get product \"image\") :alt (get product \"name\")
:class \"w-full h-48 object-cover rounded\")
(h3 :class \"mt-2 font-semibold text-stone-800\"
(get product \"name\"))
(p :class \"text-stone-500 text-sm\"
(get product \"description\"))
(div :class \"mt-3 flex items-center justify-between\"
(span :class \"text-lg font-bold\"
(format-price (get product \"price\")))
(button :class \"px-3 py-1 bg-violet-500 text-white rounded hover:bg-violet-600\"
:sx-post (str \"/cart/add/\" (get product \"id\"))
:sx-target \"#cart-count\"
\"Add to cart\"))))")
(p :class "text-stone-600"
"Structure, style, and behavior are co-located because they represent " (em "one concern") ": the product card. The component can be moved, renamed, reused, or deleted as a unit. Changing its appearance does not require editing a global stylesheet. Changing its click behavior does not require searching through a shared script file.")
(p :class "text-stone-600"
"This is not a rejection of separation of concerns. It is separation of concerns taken seriously — by the domain, not by the framework."))
(~doc-section :title "When real separation matters" :id "real-separation"
(p :class "text-stone-600"
"Genuine separation of concerns still applies, but at the right boundaries:")
(ul :class "space-y-2 text-stone-600"
(li (strong "Components from each other") " — a product card should not know about the checkout flow. They interact through props and events, not shared mutable state.")
(li (strong "Data from presentation") " — the product data comes from a service or API, not from hardcoded markup. The component receives data; it does not fetch or own it.")
(li (strong "Platform from application") " — SX's boundary spec separates host primitives from application logic. The evaluator does not know about HTTP. Page helpers do not know about the AST.")
(li (strong "Content from chrome") " — layout components (nav, footer, sidebar) are separate from content components (articles, product listings, forms). They compose, they do not intermingle."))
(p :class "text-stone-600"
"These boundaries emerge from the application's actual structure. They happen to cut across HTML, CSS, and JavaScript freely — because those categories were never meaningful to begin with."))
(~doc-section :title "The cost of the wrong separation" :id "cost"
(p :class "text-stone-600"
"The HTML/CSS/JS separation has real costs that have been absorbed so thoroughly they are invisible:")
(ul :class "space-y-2 text-stone-600"
(li (strong "Selector coupling") " — CSS selectors create implicit dependencies between stylesheets and markup. Rename a class in HTML, forget to update the CSS, and styles silently break. No compiler error. No runtime error. Just a broken layout discovered in production.")
(li (strong "Global namespace collision") " — every CSS rule lives in a global namespace. BEM, SMACSS, CSS Modules, scoped styles — these are all workarounds for a problem that only exists because styles were separated from the things they style.")
(li (strong "Shotgun surgery") " — a single feature change requires coordinated edits across HTML, CSS, and JS files. Miss one, and the feature is half-implemented. The change has a blast radius proportional to the number of technology layers, not the number of domain concerns.")
(li (strong "Dead code accumulation") " — CSS rules outlive the markup they were written for. Nobody deletes old styles because nobody can be sure what else depends on them. Stylesheets grow monotonically. Refactoring is archaeology."))
(p :class "text-stone-600"
"Every one of these problems vanishes when style, structure, and behavior are co-located in a component. Delete the component, and its styles, markup, and handlers are gone. No orphans. No archaeology."))
(~doc-section :title "The principle, stated plainly" :id "principle"
(p :class "text-stone-600"
"Separation of concerns is a domain-specific design decision. It cannot be imposed by a framework. The web platform's HTML/CSS/JS split is an implementation detail of the browser, not an architectural principle for applications. Treating it as one has cost the industry decades of unnecessary complexity, tooling, and convention.")
(p :class "text-stone-600"
"Separate the things that change for different reasons. Co-locate the things that change together. That is the entire principle. It says nothing about file extensions."))))

View File

@@ -0,0 +1,110 @@
(defcomp ~essay-server-architecture ()
(~doc-page :title "Server Architecture"
(p :class "text-stone-500 text-sm italic mb-8"
"How SX enforces the boundary between host language and embedded language, why that boundary matters, and what it looks like across different target languages.")
(~doc-section :title "The island constraint" :id "island"
(p :class "text-stone-600"
"SX is an embedded language. It runs inside a host language — for example Python on the server, JavaScript in the browser. The central architectural constraint is that SX is a " (strong "pure island") ": the evaluator sees values in and values out. No host objects leak into the SX environment. No SX expressions reach into host internals. Every interaction between SX and the host passes through a declared, validated boundary.")
(p :class "text-stone-600"
"This is not a performance optimization or a convenience. It is the property that makes self-hosting possible. If host objects can leak into SX environments, then the spec files depend on host-specific types. If SX expressions can call host functions directly, the evaluator's behavior varies per host. Neither of those is compatible with a single specification that bootstraps to multiple targets.")
(p :class "text-stone-600"
"The constraint: " (strong "nothing crosses the boundary unless it is declared in a spec file and its type is one of the boundary types") "."))
(~doc-section :title "Three tiers" :id "tiers"
(p :class "text-stone-600"
"Host functions that SX can call are organized into three tiers, each with different trust levels and declaration requirements:")
(div :class "space-y-4"
(div :class "border rounded-lg p-4 border-stone-200"
(h3 :class "font-semibold text-stone-800 mb-2" "Tier 1: Pure primitives")
(p :class "text-stone-600 text-sm"
"Declared in " (code :class "text-violet-700 text-sm" "primitives.sx") ". About 80 functions — arithmetic, string operations, list operations, dict operations, type predicates. All pure: values in, values out, no side effects. These are the only host functions visible to the spec itself. Every bootstrapper must implement all of them."))
(div :class "border rounded-lg p-4 border-stone-200"
(h3 :class "font-semibold text-stone-800 mb-2" "Tier 2: I/O primitives")
(p :class "text-stone-600 text-sm"
"Declared in " (code :class "text-violet-700 text-sm" "boundary.sx") ". Cross-service queries, fragment fetching, request context access, URL generation. Async and side-effectful. They need host context (HTTP request, database connection, config). The SX resolver identifies these in the render tree, gathers them, executes them in parallel, and substitutes results back in."))
(div :class "border rounded-lg p-4 border-stone-200"
(h3 :class "font-semibold text-stone-800 mb-2" "Tier 3: Page helpers")
(p :class "text-stone-600 text-sm"
"Also declared in " (code :class "text-violet-700 text-sm" "boundary.sx") ". Service-scoped Python functions registered via " (code :class "text-violet-700 text-sm" "register_page_helpers()") ". They provide data for specific page types — syntax highlighting, reference table data, bootstrapper output. Each helper is bound to a specific service and available only in that service's page evaluation environment."))))
(~doc-section :title "Boundary types" :id "types"
(p :class "text-stone-600"
"Only these types may cross the host-SX boundary:")
(~doc-code :code (highlight "(define-boundary-types\n (list \"number\" \"string\" \"boolean\" \"nil\" \"keyword\"\n \"list\" \"dict\" \"sx-source\" \"style-value\"))" "lisp"))
(p :class "text-stone-600"
"No Python " (code :class "text-violet-700 text-sm" "datetime") " objects. No ORM models. No Quart request objects. If a host function returns a " (code :class "text-violet-700 text-sm" "datetime") ", it must convert to an ISO string before crossing. If it returns a database row, it must convert to a plain dict. The boundary validation checks this recursively — lists and dicts have their elements checked too.")
(p :class "text-stone-600"
"The " (code :class "text-violet-700 text-sm" "sx-source") " type is SX source text wrapped in an " (code :class "text-violet-700 text-sm" "SxExpr") " marker. It allows the host to pass pre-rendered SX markup into the tree — but only the host can create it. SX code cannot construct SxExpr values; it can only receive them from the boundary."))
(~doc-section :title "Enforcement" :id "enforcement"
(p :class "text-stone-600"
"The boundary contract is enforced at three points, each corresponding to a tier:")
(div :class "space-y-3"
(div :class "bg-stone-100 rounded p-4"
(p :class "text-sm text-stone-700"
(strong "Primitive registration. ") "When " (code :class "text-violet-700 text-sm" "@register_primitive") " decorates a function, it calls " (code :class "text-violet-700 text-sm" "validate_primitive(name)") ". If the name is not declared in " (code :class "text-violet-700 text-sm" "primitives.sx") ", the service fails to start."))
(div :class "bg-stone-100 rounded p-4"
(p :class "text-sm text-stone-700"
(strong "I/O handler registration. ") "When " (code :class "text-violet-700 text-sm" "primitives_io.py") " builds the " (code :class "text-violet-700 text-sm" "_IO_HANDLERS") " dict, each name is validated against " (code :class "text-violet-700 text-sm" "boundary.sx") ". Undeclared I/O primitives crash the import."))
(div :class "bg-stone-100 rounded p-4"
(p :class "text-sm text-stone-700"
(strong "Page helper registration. ") "When a service calls " (code :class "text-violet-700 text-sm" "register_page_helpers(service, helpers)") ", each helper name is validated against " (code :class "text-violet-700 text-sm" "boundary.sx") " for that service. Undeclared helpers fail. Return values are wrapped to pass through " (code :class "text-violet-700 text-sm" "validate_boundary_value()") ".")))
(p :class "text-stone-600"
"All three checks are controlled by the " (code :class "text-violet-700 text-sm" "SX_BOUNDARY_STRICT") " environment variable. With " (code :class "text-violet-700 text-sm" "\"1\"") " (the production default), violations crash at startup. Without it, they log warnings. The strict mode is set in both " (code :class "text-violet-700 text-sm" "docker-compose.yml") " and " (code :class "text-violet-700 text-sm" "docker-compose.dev.yml") "."))
(~doc-section :title "The SX-in-Python rule" :id "sx-in-python"
(p :class "text-stone-600"
"One enforcement that is not automated but equally important: " (strong "SX source code must not be constructed as Python strings") ". S-expressions belong in " (code :class "text-violet-700 text-sm" ".sx") " files. Python belongs in " (code :class "text-violet-700 text-sm" ".py") " files. If you see a Python f-string that builds " (code :class "text-violet-700 text-sm" "(div :class ...)") ", that is a boundary violation.")
(p :class "text-stone-600"
"The correct pattern: Python returns " (strong "data") " (dicts, lists, strings). " (code :class "text-violet-700 text-sm" ".sx") " files receive data via keyword args and compose the markup. The only exception is " (code :class "text-violet-700 text-sm" "SxExpr") " wrappers for pre-rendered fragments — and those should be built with " (code :class "text-violet-700 text-sm" "sx_call()") " or " (code :class "text-violet-700 text-sm" "_sx_fragment()") ", never with f-strings.")
(~doc-code :code (highlight ";; CORRECT: .sx file composes markup from data\n(defcomp ~my-page (&key items)\n (div :class \"space-y-4\"\n (map (fn (item)\n (div :class \"border rounded p-3\"\n (h3 (get item \"title\"))\n (p (get item \"desc\"))))\n items)))" "lisp"))
(~doc-code :code (highlight "# CORRECT: Python returns data\ndef _my_page_data():\n return {\"items\": [{\"title\": \"A\", \"desc\": \"B\"}]}\n\n# WRONG: Python builds SX source\ndef _my_page_data():\n return SxExpr(f'(div (h3 \"{title}\"))') # NO" "python")))
(~doc-section :title "Why this matters for multiple languages" :id "languages"
(p :class "text-stone-600"
"The boundary contract is target-agnostic. " (code :class "text-violet-700 text-sm" "boundary.sx") " and " (code :class "text-violet-700 text-sm" "primitives.sx") " declare what crosses the boundary. Each bootstrapper reads those declarations and emits the strongest enforcement the target language supports:")
(div :class "overflow-x-auto rounded border border-stone-200 my-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" "Target")
(th :class "px-3 py-2 font-medium text-stone-600" "Enforcement")
(th :class "px-3 py-2 font-medium text-stone-600" "Mechanism")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Python")
(td :class "px-3 py-2 text-stone-600" "Runtime")
(td :class "px-3 py-2 text-stone-500 text-sm" "Frozen sets + validation at registration"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "JavaScript")
(td :class "px-3 py-2 text-stone-600" "Runtime")
(td :class "px-3 py-2 text-stone-500 text-sm" "Set guards on registerPrimitive()"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Rust")
(td :class "px-3 py-2 text-stone-600" "Compile-time")
(td :class "px-3 py-2 text-stone-500 text-sm" "SxValue enum, trait bounds on primitive fns"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Haskell")
(td :class "px-3 py-2 text-stone-600" "Compile-time")
(td :class "px-3 py-2 text-stone-500 text-sm" "ADT + typeclass constraints"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "TypeScript")
(td :class "px-3 py-2 text-stone-600" "Compile-time")
(td :class "px-3 py-2 text-stone-500 text-sm" "Discriminated union types")))))
(p :class "text-stone-600"
"In Python and JavaScript, boundary violations are caught at startup. In Rust, Haskell, or TypeScript, they would be caught at compile time — a function that returns a non-boundary type simply would not type-check. The spec is the same; the enforcement level rises with the type system.")
(p :class "text-stone-600"
"This is the payoff of the pure island constraint. Because SX never touches host internals, a bootstrapper for a new target only needs to implement the declared primitives and boundary functions. The evaluator, renderer, parser, and all components work unchanged. One spec, every target, same guarantees."))
(~doc-section :title "The spec as contract" :id "contract"
(p :class "text-stone-600"
"The boundary enforcement files form a closed contract:")
(ul :class "space-y-2 text-stone-600"
(li (code :class "text-violet-700 text-sm" "primitives.sx") " — declares every pure function SX can call")
(li (code :class "text-violet-700 text-sm" "boundary.sx") " — declares every I/O function and page helper")
(li (code :class "text-violet-700 text-sm" "boundary_parser.py") " — reads both files, extracts declared names")
(li (code :class "text-violet-700 text-sm" "boundary.py") " — runtime validation (validate_primitive, validate_io, validate_helper, validate_boundary_value)"))
(p :class "text-stone-600"
"If you add a new host function and forget to declare it, the service will not start. If you return a disallowed type, the validation will catch it. The spec files are not documentation — they are the mechanism. The bootstrappers read them. The validators parse them. The contract is enforced by the same files that describe it.")
(p :class "text-stone-600"
"This closes the loop on self-hosting. The SX spec defines the language. The boundary spec defines the edge. The bootstrappers generate implementations from both. And the generated code validates itself against the spec at startup. The spec is the implementation is the contract is the spec."))))

109
sx/sx/essays/sx-and-ai.sx Normal file
View File

@@ -0,0 +1,109 @@
(defcomp ~essay-sx-and-ai ()
(~doc-page :title "SX and AI"
(p :class "text-stone-500 text-sm italic mb-8"
"Why s-expressions are the most AI-friendly representation for web interfaces — and what that means for how software gets built.")
(~doc-section :title "The syntax tax" :id "syntax-tax"
(p :class "text-stone-600"
"Every programming language imposes a syntax tax on AI code generation. The model must produce output that satisfies a grammar — matching braces, semicolons in the right places, operator precedence, indentation rules, closing tags that match opening tags. The more complex the grammar, the more tokens the model wastes on syntactic bookkeeping instead of semantic intent.")
(p :class "text-stone-600"
"Consider what an AI must get right to produce a valid React component: JSX tags that open and close correctly, curly braces for JavaScript expressions inside markup, import statements with correct paths, semicolons or ASI rules, TypeScript type annotations, CSS-in-JS string literals with different quoting rules than the surrounding code. Each syntactic concern is a potential failure point. Each failure produces something that does not parse, let alone run.")
(p :class "text-stone-600"
"S-expressions have one syntactic form: " (code "(head args...)") ". Parentheses open and close. Strings are quoted. That is the entire grammar. There is no operator precedence because there are no operators. There is no indentation sensitivity because whitespace is not significant. There are no closing tags because there are no tags — just lists.")
(p :class "text-stone-600"
"The syntax tax for SX is essentially zero. An AI that can count parentheses can produce syntactically valid SX. This is not a small advantage — it is a categorical one. The model spends its capacity on " (em "what") " to generate, not " (em "how") " to format it."))
(~doc-section :title "One representation for everything" :id "one-representation"
(p :class "text-stone-600"
"A typical web project requires the AI to context-switch between HTML (angle brackets, void elements, boolean attributes), CSS (selectors, properties, at-rules, a completely different syntax from HTML), JavaScript (statements, expressions, classes, closures, async/await), and whatever templating language glues them together (Jinja delimiters, ERB tags, JSX interpolation). Each is a separate grammar. Each has edge cases. Each interacts with the others in ways that are hard to predict.")
(p :class "text-stone-600"
"In SX, structure, style, logic, and data are all s-expressions:")
(~doc-code :code (highlight ";; Structure\n(div :class \"card\" (h2 title) (p body))\n\n;; Style\n(cssx card-style\n :bg white :rounded-lg :shadow-md :p 6)\n\n;; Logic\n(if (> (length items) 0)\n (map render-item items)\n (p \"No items found.\"))\n\n;; Data\n{:name \"Alice\" :role \"admin\" :active true}\n\n;; Component definition\n(defcomp ~user-card (&key user)\n (div :class \"card\"\n (h2 (get user \"name\"))\n (span :class \"badge\" (get user \"role\"))))" "lisp"))
(p :class "text-stone-600"
"The AI learns one syntax and applies it everywhere. The mental model does not fragment across subsystems. A " (code "div") " and an " (code "if") " and a " (code "defcomp") " are all lists. The model that generates one can generate all three, because they are the same thing."))
(~doc-section :title "The spec fits in a context window" :id "spec-fits"
(p :class "text-stone-600"
"The complete SX language specification — evaluator, parser, renderer, primitives — lives in four files totalling roughly 3,000 lines. An AI model with a 200k token context window can hold the " (em "entire language definition") " alongside the user's codebase and still have room to work. Compare this to JavaScript (the " (a :href "https://ecma-international.org/publications-and-standards/standards/ecma-262/" :class "text-violet-600 hover:underline" "ECMAScript specification") " is 900+ pages), or the combined specifications for HTML, CSS, and the DOM.")
(p :class "text-stone-600"
"This is not just a convenience — it changes what kind of code the AI produces. When the model has the full spec in context, it does not hallucinate nonexistent features. It does not confuse one version's semantics with another's. It knows exactly which primitives exist, which special forms are available, and how evaluation works — because it is reading the authoritative definition, not interpolating from training data.")
(p :class "text-stone-600"
"The spec is also written in SX. " (code "eval.sx") " defines the evaluator as s-expressions. " (code "parser.sx") " defines the parser as s-expressions. The language the AI is generating is the same language the spec is written in. There is no translation gap between \"understanding the language\" and \"using the language\" — they are the same act of reading s-expressions."))
(~doc-section :title "Structural validation is trivial" :id "structural-validation"
(p :class "text-stone-600"
"Validating AI output before executing it is a critical safety concern. With conventional languages, validation means running a full parser, type checker, and linter — each with their own error recovery modes and edge cases. With SX, structural validation is: " (em "do the parentheses balance?") " That is it. If they balance, the expression parses. If it parses, it can be evaluated.")
(p :class "text-stone-600"
"This makes it trivial to build AI pipelines that generate SX. Parse the output. If it parses, evaluate it in a sandbox. If it does not parse, the error is always the same kind — unmatched parentheses — and the fix is always mechanical. There is no \"your JSX is invalid because you used " (code "class") " instead of " (code "className") "\" or \"you forgot the semicolon after the type annotation but before the generic constraint.\"")
(p :class "text-stone-600"
"Beyond parsing, the SX " (a :href "/specs/primitives" :class "text-violet-600 hover:underline" "boundary system") " provides semantic validation. A pure component cannot call IO primitives — not by convention, but by the evaluator refusing to resolve them. An AI generating a component can produce whatever expressions it wants; the sandbox ensures only permitted operations execute. Validation is not a separate step bolted onto the pipeline. It is the language."))
(~doc-section :title "Components are self-documenting" :id "self-documenting"
(p :class "text-stone-600"
"A React component's interface is spread across prop types (or TypeScript interfaces), JSDoc comments, Storybook stories, and whatever documentation someone wrote. An AI reading a component must synthesize information from multiple sources to understand what it accepts and what it produces.")
(p :class "text-stone-600"
"An SX component declares everything in one expression:")
(~doc-code :code (highlight "(defcomp ~product-card (&key title price image &rest children)\n (div :class \"rounded border p-4\"\n (img :src image :alt title)\n (h3 :class \"font-bold\" title)\n (span :class \"text-lg\" (format-price price))\n children))" "lisp"))
(p :class "text-stone-600"
"The AI reads this and knows: it takes " (code "title") ", " (code "price") ", and " (code "image") " as keyword arguments, and " (code "children") " as rest arguments. It knows the output structure — a " (code "div") " with an image, heading, price, and slot for children. It knows this because the definition " (em "is") " the documentation. There is no separate spec to consult, no type file to find, no ambiguity about which props are required.")
(p :class "text-stone-600"
"This self-describing property scales across the entire component environment. An AI can " (code "(map ...)") " over every component in the registry, extract all parameter signatures, build a complete map of the UI vocabulary — and generate compositions that use it correctly, because the interface is declared in the same language the AI is generating."))
(~doc-section :title "Token efficiency" :id "token-efficiency"
(p :class "text-stone-600"
"LLMs operate on tokens. Every token costs compute, latency, and money. The information density of a representation — how much semantics per token — directly affects how much an AI can see, generate, and reason about within its context window and output budget.")
(p :class "text-stone-600"
"Compare equivalent UI definitions:")
(~doc-code :code (highlight ";; SX: 42 tokens\n(div :class \"card p-4\"\n (h2 :class \"font-bold\" title)\n (p body)\n (when footer\n (div :class \"mt-4 border-t pt-2\" footer)))" "lisp"))
(~doc-code :code (highlight "// React/JSX: ~75 tokens\n<div className=\"card p-4\">\n <h2 className=\"font-bold\">{title}</h2>\n <p>{body}</p>\n {footer && (\n <div className=\"mt-4 border-t pt-2\">{footer}</div>\n )}\n</div>" "python"))
(p :class "text-stone-600"
"The SX version is roughly 40% fewer tokens for equivalent semantics. No closing tags. No curly-brace interpolation. No " (code "className") " vs " (code "class") " distinction. Every token carries meaning. Over an entire application — dozens of components, hundreds of expressions — this compounds into significantly more code visible per context window and significantly less output the model must generate."))
(~doc-section :title "Composability is free" :id "composability"
(p :class "text-stone-600"
"The hardest thing for AI to get right in conventional frameworks is composition — how pieces fit together. React has rules about hooks. Vue has template vs script vs style sections. Angular has modules, declarations, and dependency injection. Each framework's composition model is a set of conventions the AI must learn and apply correctly.")
(p :class "text-stone-600"
"S-expressions compose by nesting. A list inside a list is a composition. There are no rules beyond this:")
(~doc-code :code (highlight ";; Compose components by nesting — that's it\n(~page-layout :title \"Dashboard\"\n (~sidebar\n (~nav-menu :items menu-items))\n (~main-content\n (map ~user-card users)\n (~pagination :page current-page :total total-pages)))" "lisp"))
(p :class "text-stone-600"
"No imports to manage. No registration steps. No render props, higher-order components, or composition APIs. The AI generates a nested structure and it works, because nesting is the only composition mechanism. This eliminates an entire class of errors that plague AI-generated code in conventional frameworks — the kind where each piece works in isolation but the assembly is wrong."))
(~doc-section :title "The feedback loop" :id "feedback-loop"
(p :class "text-stone-600"
"SX has no build step. Generated s-expressions can be evaluated immediately — in the browser, on the server, in a test harness. The AI generates an expression, the system evaluates it, the result is visible. If it is wrong, the AI reads the result (also an s-expression), adjusts, and regenerates. The loop is:")
(ol :class "list-decimal pl-5 text-stone-600 space-y-1 mt-2"
(li "AI generates SX expression")
(li "System parses (parentheses balance? done)")
(li "Evaluator runs in sandbox (boundary-enforced)")
(li "Result rendered or error returned (as s-expression)")
(li "AI reads result, iterates"))
(p :class "text-stone-600"
"Compare this to the conventional loop: AI generates code → linter runs → TypeScript compiles → bundler builds → browser loads → error appears in DevTools console → human copies error back to AI → AI regenerates. Each step is a different tool with different output formats. Each introduces latency and potential information loss.")
(p :class "text-stone-600"
"The SX loop is also " (em "uniform") ". The input is s-expressions. The output is s-expressions. The error messages are s-expressions. The AI never needs to parse a stack trace format or extract meaning from a webpack error. Everything is the same data structure, all the way down."))
(~doc-section :title "This site is the proof" :id "proof"
(p :class "text-stone-600"
"This is not theoretical. Everything you are looking at — every page, every component, every line of this essay — was produced by agentic AI. Not \"AI-assisted\" in the polite sense of autocomplete suggestions. " (em "Produced.") " The SX language specification. The parser. The evaluator. The renderer. The bootstrappers that transpile the spec to JavaScript and Python. The boundary enforcement system. The dependency analyser. The on-demand CSS engine. The client-side router. The component bundler. The syntax highlighter. This documentation site. The Docker deployment. All of it.")
(p :class "text-stone-600"
"The human driving this has never written a line of Lisp. Not Common Lisp. Not Scheme. Not Clojure. Not Emacs Lisp. Has never opened the codebase in VS Code, vi, or any other editor. Every file was created and modified through " (a :href "https://claude.ai/" :class "text-violet-600 hover:underline" "Claude") " running in a terminal — reading files, writing files, running commands, iterating on errors. The development environment is a conversation.")
(p :class "text-stone-600"
"That this works at all is a testament to s-expressions. The AI generates " (code "(defcomp ~card (&key title) (div :class \"p-4\" (h2 title)))") " and it is correct on the first attempt, because there is almost nothing to get wrong. The AI generates a 300-line spec file defining evaluator semantics and every parenthesis balances, because balancing parentheses is the " (em "only") " syntactic constraint. The AI writes a bootstrapper that reads " (code "eval.sx") " and emits JavaScript, and the output runs in the browser, because the source and target are both trees.")
(p :class "text-stone-600"
"Try this with React. Try generating a complete component framework — parser, evaluator, renderer, type system, macro expander, CSS engine, client router — through pure conversation with an AI, never touching an editor. The syntax tax alone would be fatal. JSX irregularities, hook ordering rules, import resolution, TypeScript generics, webpack configuration, CSS module scoping — each is a class of errors that burns tokens and breaks the flow. S-expressions eliminate all of them."))
(~doc-section :title "The development loop" :id "dev-loop"
(p :class "text-stone-600"
"The workflow looks like this: describe what you want. The AI reads the existing code — because it can, because s-expressions are transparent to any reader. It generates new expressions. It writes them to disk. It runs the server. It checks the output. If something breaks, it reads the error, adjusts, and regenerates. The human steers with intent; the AI handles the syntax, the structure, and the mechanical correctness.")
(p :class "text-stone-600"
"This is only possible because the representation is uniform. The AI does not need to switch between \"writing HTML mode\" and \"writing CSS mode\" and \"writing JavaScript mode\" and \"writing deployment config mode.\" It is always writing s-expressions. The cognitive load is constant. The error rate is constant. The speed is constant — regardless of whether it is generating a page layout, a macro expander, or a Docker healthcheck.")
(p :class "text-stone-600"
"The " (a :href "/essays/sx-sucks" :class "text-violet-600 hover:underline" "sx sucks") " essay copped to the AI authorship and framed it as a weakness — microwave dinner on a nice plate. But the framing was wrong. If a language is so well-suited to machine generation that one person with no Lisp experience can build a self-hosting language, a multi-target bootstrapper, a reactive component framework, and a full documentation site through pure agentic AI — that is not a weakness of the language. That is the language working exactly as it should."))
(~doc-section :title "What this changes" :id "what-changes"
(p :class "text-stone-600"
"The question is not whether AI will generate user interfaces. It already does. The question is what representation makes that generation most reliable, most efficient, and most safe. S-expressions — with their zero-syntax-tax grammar, uniform structure, self-describing components, structural validation, and sandboxed evaluation — are a strong answer.")
(p :class "text-stone-600"
"Not because they were designed for AI. " (a :href "https://en.wikipedia.org/wiki/John_McCarthy_(computer_scientist)" :class "text-violet-600 hover:underline" "McCarthy") " invented them in 1958, decades before anyone imagined language models. But the properties that make s-expressions elegant for humans — minimalism, uniformity, composability, homoiconicity — turn out to be exactly the properties that make them tractable for machines. The simplest possible syntax is also the most machine-friendly syntax. This is not a coincidence. It is a consequence of what simplicity means.")
(p :class "text-stone-600"
"The era of AI-generated software is not coming. It is here. The question is which representations survive contact with it. The ones with the lowest syntax tax, the most uniform structure, and the tightest feedback loops will win — not because they are trendy, but because they are what the machines can actually produce reliably. S-expressions have been waiting sixty-seven years for a generation mechanism worthy of their simplicity. They finally have one."))))

View File

@@ -0,0 +1,112 @@
(defcomp ~essay-sx-and-dennett ()
(~doc-page :title "SX and Dennett"
(p :class "text-stone-500 text-sm italic mb-8"
"Real patterns, multiple drafts, and the intentional stance — a philosopher of mind meets a language that thinks about itself.")
(~doc-section :title "I. The intentional stance" :id "intentional-stance"
(p :class "text-stone-600"
"Daniel " (a :href "https://en.wikipedia.org/wiki/Daniel_Dennett" :class "text-violet-600 hover:underline" "Dennett") " spent fifty years arguing that the mind is not what it seems. His central method is the " (a :href "https://en.wikipedia.org/wiki/Intentional_stance" :class "text-violet-600 hover:underline" "intentional stance") " — a strategy for predicting a system's behaviour by treating it " (em "as if") " it has beliefs, desires, and intentions, whether or not it \"really\" does.")
(p :class "text-stone-600"
"There are three stances. The " (em "physical stance") " predicts from physics — voltage levels, transistor states, bytes in memory. The " (em "design stance") " predicts from how the thing was built — a thermostat turns on the heating when the temperature drops below the set point, regardless of its internal wiring. The " (em "intentional stance") " predicts from ascribed beliefs and goals — the chess program \"wants\" to protect its king, \"believes\" the centre is important.")
(p :class "text-stone-600"
"Web frameworks enforce a single stance. React's mental model is the design stance: components are functions, props go in, JSX comes out. You reason about the system by reasoning about its design. If you need the physical stance (what is actually in the DOM right now?), you reach for " (code "useRef") ". If you need the intentional stance (what does this component " (em "mean") "?), you read the documentation. Each stance requires a different tool, a different context switch.")
(p :class "text-stone-600"
"SX lets you shift stances without shifting languages. The physical stance: " (code "(div :class \"card\" (h2 \"Title\"))") " — this is exactly the DOM structure that will be produced. One list, one element. The design stance: " (code "(defcomp ~card (&key title) (div title))") " — this is how the component is built, its contract. The intentional stance: " (code "(~card :title \"Hello\")") " — this " (em "means") " \"render a card with this title,\" and you can reason about it at that level without knowing the implementation.")
(~doc-code :lang "lisp" :code
";; Physical stance — the literal structure\n(div :class \"card\" (h2 \"Title\"))\n\n;; Design stance — how it's built\n(defcomp ~card (&key title) (div :class \"card\" (h2 title)))\n\n;; Intentional stance — what it means\n(~card :title \"Title\")\n\n;; All three are s-expressions.\n;; All three can be inspected, transformed, quoted.\n;; Shifting stance = changing which expression you look at.")
(p :class "text-stone-600"
"The key insight: all three stances are expressed in the same medium. You do not need a debugger for the physical stance, a type system for the design stance, and documentation for the intentional stance. You need lists. The stances are not different tools — they are different ways of reading the same data."))
(~doc-section :title "II. Real patterns" :id "real-patterns"
(p :class "text-stone-600"
"Dennett's 1991 paper \"" (a :href "https://en.wikipedia.org/wiki/Real_Patterns" :class "text-violet-600 hover:underline" "Real Patterns") "\" makes a deceptively simple argument: a pattern is real if it lets you compress data — if recognising the pattern gives you predictive leverage that you would not have otherwise. Patterns are not " (em "in the mind") " of the observer. They are not " (em "in the object") " independently of any observer. They are real features of the world that exist at a particular level of description.")
(p :class "text-stone-600"
"Consider a bitmap of noise. If you describe it pixel by pixel, the description is as long as the image. No compression. No pattern. Now consider a bitmap of a checkerboard. You can say \"alternating black and white squares, 8x8\" — vastly shorter than the pixel-by-pixel description. The checkerboard pattern is " (em "real") ". It exists in the data. Recognising it gives you compression.")
(p :class "text-stone-600"
"Components are real patterns. " (code "(~card :title \"Hello\")") " compresses " (code "(div :class \"card\" (h2 \"Hello\"))") " — and more importantly, it compresses every instance of card-like structure across the application into a single abstraction. The component is not a convenient fiction. It is a real pattern in the codebase: a regularity that gives you predictive power. When you see " (code "~card") ", you know the structure, the styling, the contract — without expanding the definition.")
(p :class "text-stone-600"
"Macros are real patterns at a higher level. A macro like " (code "defcomp") " captures the pattern of \"name, parameters, body\" that every component shares. It compresses the regularity of component definition itself. The macro is real in exactly Dennett's sense — it captures a genuine pattern, and that pattern gives you leverage.")
(p :class "text-stone-600"
"Now here is where SX makes Dennett's argument concrete. In most languages, the reality of patterns is debatable — are classes real? Are interfaces real? Are design patterns real? You can argue either way because the patterns exist at a different level from the code. In SX, patterns " (em "are") " code. A component is a list. A macro is a function over lists. The pattern and the data it describes are the same kind of thing — s-expressions. There is no level-of-description gap. The pattern is as real as the data it compresses, because they inhabit the same ontological plane.")
(~doc-code :lang "lisp" :code
";; The data (expanded)\n(div :class \"card\"\n (h2 \"Pattern\")\n (p \"A real one.\"))\n\n;; The pattern (compressed)\n(~card :title \"Pattern\" (p \"A real one.\"))\n\n;; The meta-pattern (the definition)\n(defcomp ~card (&key title &rest children)\n (div :class \"card\" (h2 title) children))\n\n;; All three levels: data, pattern, meta-pattern.\n;; All three are lists. All three are real."))
(~doc-section :title "III. Multiple Drafts" :id "multiple-drafts"
(p :class "text-stone-600"
"In " (a :href "https://en.wikipedia.org/wiki/Consciousness_Explained" :class "text-violet-600 hover:underline" "Consciousness Explained") " (1991), Dennett proposed the " (a :href "https://en.wikipedia.org/wiki/Multiple_drafts_model" :class "text-violet-600 hover:underline" "Multiple Drafts model") " of consciousness. There is no " (a :href "https://en.wikipedia.org/wiki/Cartesian_theater" :class "text-violet-600 hover:underline" "Cartesian theater") " — no single place in the brain where \"it all comes together\" for a central observer. Instead, multiple parallel processes generate content simultaneously. Various drafts of narrative are in process at any time, some getting revised, some abandoned, some incorporated into the ongoing story. There is no master draft. There is no final audience. There is just the process of revision itself.")
(p :class "text-stone-600"
"React is a Cartesian theater. The virtual DOM is the stage. Reconciliation is the moment where \"it all comes together\" — the single canonical comparison between what was and what should be. One tree diffs against another. One algorithm produces one patch. The entire UI passes through a single bottleneck. There is a master draft, and its name is " (code "ReactDOM.render") ".")
(p :class "text-stone-600"
"SX has no theater. There are multiple drafts, genuinely parallel, with no single canonical render:")
(ul :class "list-disc pl-6 space-y-1 text-stone-600"
(li (span :class "font-semibold" "Server draft") " — Python evaluates components into SX wire format. This is a draft: it contains component calls unexpanded, slots unfilled, decisions deferred.")
(li (span :class "font-semibold" "Wire draft") " — the SX source text transmitted over HTTP. It is a draft in transit — meaningful as text, interpretable by any reader, but not yet rendered.")
(li (span :class "font-semibold" "Client draft") " — JavaScript evaluates the wire format into DOM nodes. Another draft: the browser's layout engine will revise it further (CSS computation, reflow, paint).")
(li (span :class "font-semibold" "Interaction draft") " — the user clicks, the server produces new SX, the client patches the DOM. The revision process continues. No draft is final."))
(p :class "text-stone-600"
"Each draft is a complete s-expression. Each is meaningful on its own terms. No single process \"sees\" the whole page — the server doesn't see the DOM, the client doesn't see the Python context, the browser's layout engine doesn't see the s-expressions. The page emerges from the drafting process, not from a central reconciler.")
(p :class "text-stone-600"
"This is not a metaphor stretched over engineering. It is the actual architecture. There is no virtual DOM because there is no need for a Cartesian theater. The multiple drafts model works because each draft is in the same format — s-expressions — so revision is natural. A draft can be inspected, compared, serialised, sent somewhere else, and revised further. Dennett's insight was that consciousness works this way. SX's insight is that rendering can too."))
(~doc-section :title "IV. Heterophenomenology" :id "heterophenomenology"
(p :class "text-stone-600"
(a :href "https://en.wikipedia.org/wiki/Heterophenomenology" :class "text-violet-600 hover:underline" "Heterophenomenology") " is Dennett's method for studying consciousness. Instead of asking \"what is it like to be a bat?\" — a question we cannot answer — we ask the bat to tell us, and then we study " (em "the report") ". We take the subject's testimony seriously, catalogue it rigorously, but we do not take it as infallible. The report is data. We are scientists of the report.")
(p :class "text-stone-600"
"Most programming languages cannot report on themselves. JavaScript can " (code "toString()") " a function, but the result is a string — opaque, unparseable, implementation-dependent. Python can inspect a function's AST via " (code "ast.parse(inspect.getsource(f))") " — but the AST is a separate data structure, disconnected from the running code. The language's self-report is in a different format from the language itself. Studying it requires tools, transformations, bridges.")
(p :class "text-stone-600"
"SX is natively heterophenomenological. The language's self-report " (em "is") " the language. " (code "eval.sx") " is the evaluator reporting on how evaluation works — in the same s-expressions that it evaluates. " (code "parser.sx") " is the parser reporting on how parsing works — in the same syntax it parses. You study the report by reading it. You verify the report by running it. The report and the reality are the same object.")
(~doc-code :lang "lisp" :code
";; The evaluator's self-report (from eval.sx):\n(define eval-expr\n (fn (expr env)\n (cond\n (number? expr) expr\n (string? expr) expr\n (symbol? expr) (env-get env expr)\n (list? expr) (eval-list expr env)\n :else (error \"Unknown expression type\"))))\n\n;; This is simultaneously:\n;; 1. A specification (what eval-expr does)\n;; 2. A program (it runs)\n;; 3. A report (the evaluator describing itself)\n;; Heterophenomenology without the hetero.")
(p :class "text-stone-600"
"Dennett insisted that heterophenomenology is the only honest method. First-person reports are unreliable — introspection gets things wrong. Third-person observation misses the subject's perspective. The middle path is to take the report as data and study it rigorously. SX's self-hosting spec is this middle path enacted in code: neither a first-person account (\"trust me, this is how it works\") nor a third-person observation (English prose describing the implementation), but a structured report that can be verified, compiled, and run."))
(~doc-section :title "V. Where am I?" :id "where-am-i"
(p :class "text-stone-600"
"Dennett's thought experiment \"" (a :href "https://en.wikipedia.org/wiki/Where_Am_I%3F_(Dennett)" :class "text-violet-600 hover:underline" "Where Am I?") "\" imagines his brain removed from his body, connected by radio. His body walks around; his brain sits in a vat. Where is Dennett? Where the brain is? Where the body is? The question has no clean answer because identity is not located in a single place — it is distributed across the system.")
(p :class "text-stone-600"
"Where is an SX component? On the server, it is a Python object — a closure with a body and bound environment. On the wire, it is text: " (code "(~card :title \"Hello\")") ". In the browser, it is a JavaScript function registered in the component environment. In the DOM, it is a tree of elements. Which one is the \"real\" component? All of them. None of them. The component is not located in one runtime — it is the pattern that persists across all of them.")
(p :class "text-stone-600"
"This is Dennett's point about personal identity applied to software identity. The SX component " (code "~card") " is defined in a " (code ".sx") " file, compiled by the Python bootstrapper into the server evaluator, transmitted as SX wire format to the browser, compiled by the JavaScript bootstrapper into the client evaluator, and rendered into DOM. At every stage, it is " (code "~card") ". At no single stage is it " (em "the") " " (code "~card") ". The identity is the pattern, not the substrate.")
(p :class "text-stone-600"
"Most frameworks bind component identity to a substrate. A React component is a JavaScript function. Full stop. It cannot exist outside the JavaScript runtime. Its identity is its implementation. SX components have substrate independence — the same definition runs on any host that implements the SX platform interface. The component's identity is its specification, not its execution."))
(~doc-section :title "VI. Competence without comprehension" :id "competence"
(p :class "text-stone-600"
"Dennett argued in " (a :href "https://en.wikipedia.org/wiki/From_Bacteria_to_Bach_and_Back" :class "text-violet-600 hover:underline" "From Bacteria to Bach and Back") " (2017) that evolution produces " (em "competence without comprehension") ". Termites build elaborate mounds without understanding architecture. Neurons produce consciousness without understanding thought. The competence is real — the mound regulates temperature, the brain solves problems — but there is no comprehension anywhere in the system. No termite has a blueprint. No neuron knows it is thinking.")
(p :class "text-stone-600"
"A macro is competence without comprehension. " (code "defcomp") " expands into a component registration — it " (em "does") " the right thing — but it does not \"know\" what a component is. It is a pattern-matching function on lists that produces other lists. The expansion is mechanical, local, uncomprehending. Yet the result is a fully functional component that participates in the rendering pipeline, responds to props, composes with other components. Competence. No comprehension.")
(~doc-code :lang "lisp" :code
";; defcomp is a macro — mechanical list transformation\n(defmacro defcomp (name params &rest body)\n `(define ,name (make-component ,params ,@body)))\n\n;; It does not \"understand\" components.\n;; It rearranges symbols according to a rule.\n;; The resulting component works perfectly.\n;; Competence without comprehension.")
(p :class "text-stone-600"
"The bootstrap compiler is another level of the same phenomenon. " (code "bootstrap_js.py") " reads " (code "eval.sx") " and emits JavaScript. It does not understand SX semantics — it applies mechanical transformation rules to s-expression ASTs. Yet its output is a correct, complete SX evaluator. The compiler is competent (it produces working code) without being comprehending (it has no model of what SX expressions mean).")
(p :class "text-stone-600"
"Dennett used this insight to deflate the mystery of intelligence: you do not need a homunculus — a little man inside the machine who \"really\" understands — you just need enough competence at each level. SX embodies this architecturally. No part of the system comprehends the whole. The parser does not know about rendering. The evaluator does not know about HTTP. The bootstrap compiler does not know about the DOM. Each part is a competent specialist. The system works because the parts compose, not because any part understands the composition."))
(~doc-section :title "VII. Intuition pumps" :id "intuition-pumps"
(p :class "text-stone-600"
"Dennett called his thought experiments \"" (a :href "https://en.wikipedia.org/wiki/Intuition_pump" :class "text-violet-600 hover:underline" "intuition pumps") "\" — devices for moving your intuitions from one place to another, making the unfamiliar familiar by analogy. Not proofs. Not arguments. Machines for changing how you see.")
(p :class "text-stone-600"
"SX components are intuition pumps. A " (code "defcomp") " definition is not just executable code — it is a device for showing someone what a piece of UI " (em "is") ". Reading " (code "(defcomp ~card (&key title &rest children) (div :class \"card\" (h2 title) children))") " tells you the contract, the structure, and the output in a single expression. It pumps your intuition about what \"card\" means in this application.")
(p :class "text-stone-600"
"Compare this to a React component:")
(~doc-code :lang "lisp" :code
";; React: you must simulate the runtime in your head\nfunction Card({ title, children }) {\n return (\n <div className=\"card\">\n <h2>{title}</h2>\n {children}\n </div>\n );\n}\n\n;; SX: the definition IS the output\n(defcomp ~card (&key title &rest children)\n (div :class \"card\"\n (h2 title)\n children))")
(p :class "text-stone-600"
"The React version requires you to know that JSX compiles to createElement calls, that className maps to the class attribute, that curly braces switch to JavaScript expressions, and that the function return value becomes the rendered output. You must simulate a compiler in your head. The SX version requires you to know that lists are expressions and keywords are attributes. The gap between the definition and what it produces is smaller. The intuition pump is more efficient — fewer moving parts, less machinery between the reader and the meaning.")
(p :class "text-stone-600"
"Dennett valued intuition pumps because philosophy is full of false intuitions. The Cartesian theater feels right — of course there is a place where consciousness happens. But it is wrong. Intuition pumps help you " (em "see") " that it is wrong by giving you a better picture. SX is an intuition pump for web development: of course you need a build step, of course you need a virtual DOM, of course you need separate languages for structure and style and behaviour. But you don't. The s-expression is the better picture."))
(~doc-section :title "VIII. The Joycean machine" :id "joycean-machine"
(p :class "text-stone-600"
"In Consciousness Explained, Dennett describes the brain as a \"" (a :href "https://en.wikipedia.org/wiki/Consciousness_Explained" :class "text-violet-600 hover:underline" "Joycean machine") "\" — a virtual machine running on the parallel hardware of the brain, producing the serial narrative of conscious experience. Just as a word processor is a virtual machine running on silicon, consciousness is a virtual machine running on neurons. The virtual machine is real — it does real work, produces real effects — even though it is implemented in a substrate that knows nothing about it.")
(p :class "text-stone-600"
"SX is a Joycean machine running on the web. The web's substrate — TCP/IP, HTTP, the DOM, JavaScript engines — knows nothing about s-expressions, components, or evaluators. Yet the SX virtual machine runs on this substrate, producing real pages, real interactions, real applications. The substrate provides the physical-stance machinery. SX provides the intentional-stance narrative: this is a card, this is a page, this is a layout, this composes with that.")
(p :class "text-stone-600"
"The deeper parallel: Dennett argued that the Joycean machine is " (em "not an illusion") ". The serial narrative of consciousness is not fake — it is the real output of real processing, even though the underlying hardware is parallel and narrativeless. Similarly, SX's component model is not a convenient fiction layered over \"real\" HTML. It is the real structure of the application. The components are the thing. The HTML is the substrate, not the reality.")
(p :class "text-stone-600"
"And like Dennett's Joycean machine, SX's virtual machine can reflect on itself. It can inspect its own running code, define its own evaluator, test its own semantics. The virtual machine is aware of itself — not in the sense of consciousness, but in the functional sense of self-modelling. The spec models the evaluator. The evaluator runs the spec. The virtual machine contains a description of itself, and that description works."))
(~doc-section :title "IX. Quining" :id "quining"
(p :class "text-stone-600"
"Dennett borrowed the term \"" (a :href "https://en.wikipedia.org/wiki/Qualia#Dennett's_criticism" :class "text-violet-600 hover:underline" "quining") "\" from the logician " (a :href "https://en.wikipedia.org/wiki/Willard_Van_Orman_Quine" :class "text-violet-600 hover:underline" "W. V. O. Quine") " — a philosopher who argued that many seemingly deep concepts dissolve under scrutiny. Dennett \"quined\" qualia — the supposedly irreducible subjective qualities of experience — arguing that they are not what they seem, that the intuition of an inner experiential essence is a philosophical illusion.")
(p :class "text-stone-600"
"The concept of a \"" (a :href "https://en.wikipedia.org/wiki/Quine_(computing)" :class "text-violet-600 hover:underline" "quine") "\" in computing is related: a program that outputs its own source code. The word honours the same Quine, for the same reason — self-reference that collapses the distinction between describer and described.")
(p :class "text-stone-600"
"SX quines in both senses. In the computing sense: the self-hosting spec is a quine-like structure — " (code "eval.sx") " is an SX program that, when compiled and run, produces an evaluator capable of running " (code "eval.sx") ". It is not a literal quine (it doesn't output itself character-for-character), but it has the essential quine property: the output contains the input.")
(p :class "text-stone-600"
"In Dennett's philosophical sense: SX quines the web's qualia. The supposedly irreducible essences of web development — \"components,\" \"state,\" \"the DOM,\" \"the server-client boundary\" — dissolve under SX's scrutiny into what they always were: data structures. Lists of symbols. Expressions that evaluate to other expressions. The qualia of web development are not irreducible. They are patterns in s-expressions, and once you see that, you cannot unsee it.")
(p :class "text-stone-600"
"Dennett's lifelong project was to show that the mind is not what our intuitions say it is — that consciousness, free will, and the self are real phenomena that do not require the metaphysical foundations we instinctively assign them. They are " (a :href "https://en.wikipedia.org/wiki/Real_Patterns" :class "text-violet-600 hover:underline" "real patterns") " in physical processes, not ghostly essences hovering above matter.")
(p :class "text-stone-600"
"SX makes the same move for the web. Components are real patterns, not framework essences. The server-client boundary is a draft boundary, not an ontological divide. The build step is a habit, not a necessity. The virtual DOM is a Cartesian theater, not a requirement. Strip away the false intuitions, and what remains is what was always there: expressions, evaluation, and composition. All the way down."))))

View File

@@ -0,0 +1,93 @@
(defcomp ~essay-sx-and-wittgenstein ()
(~doc-page :title "SX and Wittgenstein"
(p :class "text-stone-500 text-sm italic mb-8"
"The limits of my language are the limits of my world.")
(~doc-section :title "I. Language games" :id "language-games"
(p :class "text-stone-600"
"In 1953, Ludwig " (a :href "https://en.wikipedia.org/wiki/Ludwig_Wittgenstein" :class "text-violet-600 hover:underline" "Wittgenstein") " published " (a :href "https://en.wikipedia.org/wiki/Philosophical_Investigations" :class "text-violet-600 hover:underline" "Philosophical Investigations") " — a book that dismantled the theory of language he had built in his own earlier work. The " (a :href "https://en.wikipedia.org/wiki/Tractatus_Logico-Philosophicus" :class "text-violet-600 hover:underline" "Tractatus") " had argued that language pictures the world: propositions mirror facts, and the structure of a sentence corresponds to the structure of reality. The Investigations abandoned this. Language does not picture anything. Language is " (em "used") ".")
(p :class "text-stone-600"
"Wittgenstein replaced the picture theory with " (a :href "https://en.wikipedia.org/wiki/Language_game_(philosophy)" :class "text-violet-600 hover:underline" "language games") " — activities in which words get their meaning from how they are employed, not from what they refer to. \"Slab!\" on a building site means \"bring me a slab.\" The same word in a dictionary means nothing until it enters a game. Meaning is use.")
(p :class "text-stone-600"
"Web development is a proliferation of language games. HTML is one game — a markup game where tags denote structure. CSS is another — a declaration game where selectors denote style. JavaScript is a third — an imperative game where statements denote behaviour. JSX is a fourth game layered on top of the third, pretending to be the first. TypeScript is a fifth game that annotates the third. Each has its own grammar, its own rules, its own way of meaning.")
(p :class "text-stone-600"
"SX collapses these into a single game. " (code "(div :class \"p-4\" (h2 title))") " is simultaneously structure (a div containing an h2), style (the class attribute), and behaviour (the symbol " (code "title") " is evaluated). There is one syntax, one set of rules, one way of meaning. Not because the distinctions between structure, style, and behaviour have been erased — they haven't — but because they are all expressed in the same language game."))
(~doc-section :title "II. The limits of my language" :id "limits"
(p :class "text-stone-600"
"\"" (a :href "https://en.wikipedia.org/wiki/Tractatus_Logico-Philosophicus" :class "text-violet-600 hover:underline" "Die Grenzen meiner Sprache bedeuten die Grenzen meiner Welt") "\" — the limits of my language mean the limits of my world. This is proposition 5.6 of the Tractatus, and it is the most important sentence Wittgenstein ever wrote.")
(p :class "text-stone-600"
"If your language is HTML, your world is documents. You can link documents. You can nest elements inside documents. You cannot compose documents from smaller documents without a server-side include, an iframe, or JavaScript. The language does not have composition, so your world does not have composition.")
(p :class "text-stone-600"
"If your language is React, your world is components that re-render. You can compose components. You can pass props. You cannot inspect a component's structure at runtime without React DevTools. You cannot serialize a component tree to a format another framework can consume. You cannot send a component over HTTP and have it work on the other side without the same React runtime. The language has composition but not portability, so your world has composition but not portability.")
(p :class "text-stone-600"
"If your language is s-expressions, your world is " (em "expressions") ". An expression can represent a DOM node, a function call, a style declaration, a macro transformation, a component definition, a wire-format payload, or a specification of the evaluator itself. The language has no built-in limits on what can be expressed, because the syntax — the list — can represent anything. The limits of the language are only the limits of what you choose to evaluate.")
(~doc-code :lang "lisp" :code
";; The same syntax expresses everything:\n(div :class \"card\" (h2 \"Title\")) ;; structure\n(css :flex :gap-4 :p-2) ;; style\n(defcomp ~card (&key title) (div title)) ;; abstraction\n(defmacro ~log (x) `(console.log ,x)) ;; metaprogramming\n(quote (div :class \"card\" (h2 \"Title\"))) ;; data about structure")
(p :class "text-stone-600"
"Wittgenstein's proposition cuts both ways. A language that limits you to documents limits your world to documents. A language that can express anything — because its syntax is the minimal recursive structure — limits your world to " (em "everything") "."))
(~doc-section :title "III. Whereof one cannot speak" :id "silence"
(p :class "text-stone-600"
"The Tractatus ends: \"" (a :href "https://en.wikipedia.org/wiki/Tractatus_Logico-Philosophicus#Proposition_7" :class "text-violet-600 hover:underline" "Whereof one cannot speak, thereof one must be silent") ".\" Proposition 7. The things that cannot be said in a language simply do not exist within that language's world.")
(p :class "text-stone-600"
"HTML cannot speak of composition. It is silent on components. You cannot define " (code "~card") " in HTML. You can define " (code "<template>") " and " (code "<slot>") " in Web Components, but that requires JavaScript — you have left HTML's language game and entered another.")
(p :class "text-stone-600"
"CSS cannot speak of conditions. It is silent on logic. You cannot say \"if the user is logged in, use this colour.\" You can use " (code ":has()") " and " (code "@container") " queries, but these are conditions about " (em "the document") ", not conditions about " (em "the application") ". CSS can only speak of what CSS can see.")
(p :class "text-stone-600"
"JavaScript can speak of almost everything — but it speaks in statements, not expressions. The difference matters. A statement executes and is gone. An expression evaluates to a value. Values compose. Statements require sequencing. React discovered this when it moved from class components (imperative, statement-oriented) to hooks (closer to expressions, but not quite — hence the rules of hooks).")
(p :class "text-stone-600"
"S-expressions are pure expression. Every form evaluates to a value. There is nothing that cannot be spoken, because lists can nest arbitrarily and symbols can name anything. There is no proposition 7 for s-expressions — no enforced silence, no boundary where the language gives out. The programmer decides where to draw the line, not the syntax."))
(~doc-section :title "IV. Family resemblance" :id "family-resemblance"
(p :class "text-stone-600"
"Wittgenstein argued that concepts do not have sharp definitions. What is a \"" (a :href "https://en.wikipedia.org/wiki/Family_resemblance" :class "text-violet-600 hover:underline" "game") "\"? Chess, football, solitaire, ring-a-ring-o'-roses — they share no single essential feature. Instead, they form a network of overlapping similarities. A " (a :href "https://en.wikipedia.org/wiki/Family_resemblance" :class "text-violet-600 hover:underline" "family resemblance") ".")
(p :class "text-stone-600"
"Web frameworks have this property. What is a \"component\"? In React, it is a function that returns JSX. In Vue, it is an object with a template property. In Svelte, it is a " (code ".svelte") " file. In Web Components, it is a class that extends HTMLElement. In Angular, it is a TypeScript class with a decorator. These are not the same thing. They share a family resemblance — they all produce reusable UI — but their definitions are incompatible. A React component cannot be used in Vue. A Svelte component cannot be used in Angular. The family does not communicate.")
(p :class "text-stone-600"
"In SX, a component is a list whose first element is a symbol beginning with " (code "~") ". That is the complete definition. It is not a function, not a class, not a file, not a decorator. It is a " (em "naming convention on a data structure") ". Any system that can process lists can process SX components. Python evaluates them on the server. JavaScript evaluates them in the browser. A future Rust evaluator could evaluate them on an embedded device. The family resemblance sharpens into actual identity: a component is a component is a component, because the representation is the same everywhere.")
(~doc-code :lang "lisp" :code
";; This is a component in every SX evaluator:\n(defcomp ~greeting (&key name)\n (div :class \"p-4\"\n (h2 (str \"Hello, \" name))))\n\n;; The same s-expression is:\n;; - parsed by the same parser\n;; - evaluated by the same eval rules\n;; - rendered by the same render spec\n;; - on every host, in every context"))
(~doc-section :title "V. Private language" :id "private-language"
(p :class "text-stone-600"
"The " (a :href "https://en.wikipedia.org/wiki/Private_language_argument" :class "text-violet-600 hover:underline" "private language argument") " is one of Wittgenstein's most provocative claims: there can be no language whose words refer to the speaker's private sensations and nothing else. Language requires public criteria — shared rules that others can check. A word that means something only to you is not a word at all.")
(p :class "text-stone-600"
"Most web frameworks are private languages. React's JSX is meaningful only to the React runtime. Vue's " (code ".vue") " single-file components are meaningful only to the Vue compiler. Svelte's " (code ".svelte") " files are meaningful only to the Svelte compiler. Each framework speaks a language that no one else can understand. They are private languages in Wittgenstein's sense — their terms have meaning only within their own closed world.")
(p :class "text-stone-600"
"S-expressions are radically public. The syntax is universal: open paren, atoms, close paren. Any Lisp, any s-expression processor, any JSON-to-sexp converter can read them. The SX evaluator adds meaning — " (code "defcomp") ", " (code "defmacro") ", " (code "if") ", " (code "let") " — but these meanings are specified in s-expressions themselves (" (code "eval.sx") "), readable by anyone. There is no private knowledge. There is no compilation step that transforms the public syntax into a private intermediate form. The source is the artefact.")
(p :class "text-stone-600"
"This is why SX can be self-hosting. A private language cannot define itself — it would need a second private language to define the first, and a third to define the second. A public language, one whose rules are expressible in its own terms, can close the loop. " (code "eval.sx") " defines SX in SX. The language defines itself publicly, in a form that any reader can inspect."))
(~doc-section :title "VI. Showing and saying" :id "showing-saying"
(p :class "text-stone-600"
"The Tractatus makes a crucial distinction between what can be " (em "said") " and what can only be " (em "shown") ". Logic, Wittgenstein argued, cannot be said — it can only be shown by the structure of propositions. You cannot step outside logic to make statements about logic; you can only exhibit logical structure by using it.")
(p :class "text-stone-600"
"Most languages " (em "say") " their semantics — in English-language specifications, in RFC documents, in MDN pages. The semantics are described " (em "about") " the language, in a different medium.")
(p :class "text-stone-600"
"SX " (em "shows") " its semantics. The specification is not a description of the language — it is the language operating on itself. " (code "eval.sx") " does not say \"the evaluator dispatches on the type of expression.\" It " (em "is") " an evaluator that dispatches on the type of expression. " (code "parser.sx") " does not say \"strings are delimited by double quotes.\" It " (em "is") " a parser that recognises double-quoted strings.")
(p :class "text-stone-600"
"This is exactly the distinction Wittgenstein drew. What can be said (described, documented, specified in English) is limited. What can be shown (exhibited, demonstrated, enacted) goes further. SX's self-hosting spec shows its semantics by " (em "being") " them — the strongest form of specification possible. The spec cannot be wrong, because the spec runs."))
(~doc-section :title "VII. The beetle in the box" :id "beetle"
(p :class "text-stone-600"
"Wittgenstein's " (a :href "https://en.wikipedia.org/wiki/Private_language_argument#The_beetle_in_a_box" :class "text-violet-600 hover:underline" "beetle-in-a-box") " thought experiment: suppose everyone has a box, and everyone calls what's inside their box a \"beetle.\" No one can look in anyone else's box. The word \"beetle\" refers to whatever is in the box — but since no one can check, the contents might be different for each person, or the box might even be empty. The word gets its meaning from the " (em "game") " it plays in public, not from the private contents of the box.")
(p :class "text-stone-600"
"A web component is a beetle in a box. You call it " (code "<my-button>") " but what's inside — Shadow DOM, event listeners, internal state, style encapsulation — is private. Two components with the same tag name might do completely different things. Two frameworks with the same concept of \"component\" might mean completely different things by it. The word \"component\" functions in the language game of developer conversation, but the actual contents are private to each implementation.")
(p :class "text-stone-600"
"In SX, you can open the box. Components are data. " (code "(defcomp ~card (&key title) (div title))") " — the entire definition is visible, inspectable, serializable. There is no shadow DOM, no hidden state machine, no encapsulated runtime. The component's body is an s-expression. You can quote it, transform it, analyse it, send it over HTTP, evaluate it in a different context. The beetle is on the table."))
(~doc-section :title "VIII. The fly-bottle" :id "fly-bottle"
(p :class "text-stone-600"
"\"What is your aim in philosophy?\" Wittgenstein was asked. \"" (a :href "https://en.wikipedia.org/wiki/Philosophical_Investigations#Meaning_and_definition" :class "text-violet-600 hover:underline" "To show the fly the way out of the fly-bottle") ".\"")
(p :class "text-stone-600"
"The fly-bottle is the trap of false problems — questions that seem deep but are actually artefacts of confused language. \"What is the virtual DOM?\" is a fly-bottle question. The virtual DOM exists because React needed a way to reconcile its component model with the browser's DOM. If your component model " (em "is") " the DOM — if components evaluate directly to DOM nodes, as they do in SX — the question dissolves. There is no virtual DOM because there is no gap between the component and what it produces.")
(p :class "text-stone-600"
"\"How do we solve CSS scoping?\" Fly-bottle. CSS scoping is a problem because CSS is a global language applied to a local context. If styles are expressions evaluated in the same scope as the component that uses them — as in SX's CSSX — there is nothing to scope. The problem was created by the language separation, not by the nature of styling.")
(p :class "text-stone-600"
"\"How do we share state between server and client?\" Fly-bottle. This is hard when the server speaks one language (Python templates) and the client speaks another (JavaScript). When both speak s-expressions, and the wire format IS the source syntax, state transfer is serialisation — which is identity for s-expressions. " (code "aser") " serialises an SX expression as an SX expression. The server and client share state by sharing code.")
(p :class "text-stone-600"
"SX does not solve these problems. It dissolves them — by removing the language confusion that created them. Wittgenstein's method, applied to web development: the problems were never real. They were artefacts of speaking in the wrong language."))
(~doc-section :title "IX. Back to rough ground" :id "rough-ground"
(p :class "text-stone-600"
"\"We have got on to slippery ice where there is no friction and so in a certain sense the conditions are ideal, but also, just because of that, we are unable to walk. We want to walk: so we need " (a :href "https://en.wikipedia.org/wiki/Philosophical_Investigations" :class "text-violet-600 hover:underline" "friction") ". Back to the rough ground!\"")
(p :class "text-stone-600"
"The slippery ice is the idealised abstraction — the framework that promises a perfect developer experience, the type system that promises to catch all bugs, the architecture diagram that promises clean separation. These are frictionless surfaces. They look beautiful. You cannot walk on them.")
(p :class "text-stone-600"
"SX is rough ground. S-expressions are not pretty. Parentheses are friction. There is no syntax highlighting tuned for this. There is no IDE with SX autocomplete. There is no build system to smooth over rough edges because there is no build system. You write s-expressions. You evaluate them. You see the result. The feedback loop is immediate and unmediated.")
(p :class "text-stone-600"
"This is a feature. Friction is how you know you are touching the ground. Friction is where the work happens. The perfectly frictionless framework lets you get started in five minutes and spend six months debugging its abstractions. SX asks you to understand s-expressions — a syntax that has not changed since 1958 — and then gives you the entire web as your world.")
(p :class "text-stone-600"
"Wittgenstein wanted philosophy to stop theorising and start looking at how language actually works. SX wants web development to stop abstracting and start looking at what expressions actually are. Lists. Symbols. Evaluation. Composition. Everything else is a fly-bottle."))))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
sx/sx/essays/sx-sucks.sx Normal file
View File

@@ -0,0 +1,2 @@
(defcomp ~essay-sx-sucks ()
(~doc-page :title "sx sucks" (~doc-section :title "The parentheses" :id "parens" (p :class "text-stone-600" "S-expressions are parentheses. Lots of parentheses. You thought LISP was dead? No, someone just decided to use it for HTML templates. Your IDE will need a parenthesis counter. Your code reviews will be 40% closing parens. Every merge conflict will be about whether a paren belongs on this line or the next.")) (~doc-section :title "Nobody asked for this" :id "nobody-asked" (p :class "text-stone-600" "The JavaScript ecosystem has React, Vue, Svelte, Solid, Qwik, and approximately 47,000 other frameworks. htmx proved you can skip them all. sx looked at this landscape and said: you know what this needs? A Lisp dialect. For HTML. Over HTTP.") (p :class "text-stone-600" "Nobody was asking for this. The zero GitHub stars confirm it. It is not even on GitHub.")) (~doc-section :title "The author has never written a line of LISP" :id "no-lisp" (p :class "text-stone-600" "The author of sx has never written a single line of actual LISP. Not Common Lisp. Not Scheme. Not Clojure. Not even Emacs Lisp. The entire s-expression evaluator was written by someone whose mental model of LISP comes from reading the first three chapters of SICP and then closing the tab.") (p :class "text-stone-600" "This is like building a sushi restaurant when your only experience with Japanese cuisine is eating supermarket California rolls.")) (~doc-section :title "AI wrote most of it" :id "ai" (p :class "text-stone-600" "A significant portion of sx — the evaluator, the parser, the primitives, the CSS scanner, this very documentation site — was written with AI assistance. The author typed prompts. Claude typed code. This is not artisanal hand-crafted software. This is the software equivalent of a microwave dinner presented on a nice plate.") (p :class "text-stone-600" "He adds features by typing stuff like \"is there rom for macros within sx.js? what benefits m,ight that bring?\", skim-reading the response, and then entering \"crack on then!\" This is not software engineering. This is improv comedy with a compiler.") (p :class "text-stone-600" "Is that bad? Maybe. Is it honest? Yes. Is this paragraph also AI-generated? You will never know.")) (~doc-section :title "No ecosystem" :id "ecosystem" (p :class "text-stone-600" "npm has 2 million packages. PyPI has 500,000. sx has zero packages, zero plugins, zero middleware, zero community, zero Stack Overflow answers, and zero conference talks. If you get stuck, your options are: read the source, or ask the one person who wrote it.") (p :class "text-stone-600" "That person is busy. Good luck.")) (~doc-section :title "Zero jobs" :id "jobs" (p :class "text-stone-600" "Adding sx to your CV will not get you hired. It will get you questioned.") (p :class "text-stone-600" "The interview will end shortly after.")) (~doc-section :title "The creator thinks s-expressions are a personality trait" :id "personality" (p :class "text-stone-600" "Look at this documentation site. It has a violet colour scheme. It has credits to htmx. It has a future possibilities page about hypothetical sx:// protocol schemes. The creator built an entire microservice — with Docker, Redis, and a custom entrypoint script — just to serve documentation about a rendering engine that runs one website.") (p :class "text-stone-600" "This is not engineering. This is a personality disorder expressed in YAML."))))

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
(defcomp ~essay-why-sexps ()
(~doc-page :title "Why S-Expressions Over HTML Attributes" (~doc-section :title "The problem with HTML attributes" :id "problem" (p :class "text-stone-600" "HTML attributes are strings. You can put anything in a string. htmx puts DSLs in strings — trigger modifiers, swap strategies, CSS selectors. This works but it means you're parsing a language within a language within a language.") (p :class "text-stone-600" "S-expressions are already structured. Keywords are keywords. Lists are lists. Nested expressions nest naturally. There's no need to invent a trigger modifier syntax because the expression language already handles composition.")) (~doc-section :title "Components without a build step" :id "components" (p :class "text-stone-600" "React showed that components are the right abstraction for UI. The price: a build step, a bundler, JSX transpilation. With s-expressions, defcomp is just another form in the language. No transpiler needed. The same source runs on server and client.")) (~doc-section :title "When attributes are better" :id "better" (p :class "text-stone-600" "HTML attributes work in any HTML document. S-expressions need a runtime. If you want progressive enhancement that works with JS disabled, htmx is better. If you want to write HTML by hand in static files, htmx is better. sx only makes sense when you're already rendering server-side and want components."))))

View File

@@ -0,0 +1,138 @@
(defcomp ~essay-zero-tooling ()
(~doc-page :title "Tools for Fools"
(p :class "text-stone-500 text-sm italic mb-8"
"SX was built without a code editor. No IDE, no manual source edits, no build tools, no linters, no bundlers. The entire codebase — evaluator, renderer, spec, documentation site, test suite — was produced through conversation with an agentic AI. This is what zero-tooling web development looks like.")
(~doc-section :title "No code editor" :id "no-editor"
(p :class "text-stone-600"
"This needs to be stated plainly, because it sounds like an exaggeration: " (strong "not a single line of SX source code was written by hand in a code editor") ". Every component definition, every page route, every essay (including this one), every test case, every spec file — all of it was produced through natural-language conversation with Claude Code, an agentic AI that reads, writes, and modifies files on the developer's behalf.")
(p :class "text-stone-600"
"No VS Code. No Vim. No Emacs. No Sublime Text. No cursor blinking in a source file. The developer describes intent; the AI produces the code, edits the files, runs the tests, and iterates. The code editor — the tool that has defined software development for sixty years — was not used.")
(p :class "text-stone-600"
"This is not a stunt. It is a consequence of two properties converging: a language with trivial syntax that AI can produce flawlessly, and an AI agent capable of understanding and modifying an entire codebase through conversation. Neither property alone would be sufficient. Together, they make the code editor unnecessary."))
(~doc-section :title "The toolchain that wasn't" :id "toolchain"
(p :class "text-stone-600"
"A modern web application typically requires a stack of tooling before a single feature can be built. Consider what a React project demands:")
(ul :class "space-y-2 text-stone-600"
(li (strong "Bundler") " — Webpack, Vite, Rollup, or esbuild to resolve modules, tree-shake dead code, split bundles, and minify output.")
(li (strong "Transpiler") " — Babel or SWC to transform JSX into function calls, strip TypeScript annotations, and downlevel modern syntax for older browsers.")
(li (strong "Package manager") " — npm, yarn, or pnpm to manage hundreds or thousands of transitive dependencies in " (code "node_modules") ".")
(li (strong "CSS pipeline") " — Sass or PostCSS to preprocess styles, CSS Modules or styled-components for scoping, Tailwind CLI for utility generation, PurgeCSS to remove unused rules.")
(li (strong "Dev server") " — webpack-dev-server or Vite's dev mode for hot module replacement, WebSocket-based live reload, and proxy configuration.")
(li (strong "Linter") " — ESLint with dozens of plugins and hundreds of rules to catch mistakes the language grammar permits.")
(li (strong "Formatter") " — Prettier to enforce consistent style, because the syntax admits so many equivalent representations that teams cannot agree without automation.")
(li (strong "Type checker") " — TypeScript compiler running in parallel to catch type errors that JavaScript's dynamic semantics would defer to runtime.")
(li (strong "Test runner") " — Jest or Vitest with jsdom or happy-dom to simulate a browser environment, plus Cypress or Playwright for integration tests.")
(li (strong "Framework CLI") " — create-react-app, Next.js CLI, or Angular CLI to scaffold project structure, generate boilerplate, and manage framework-specific configuration."))
(p :class "text-stone-600"
"That is ten categories of tooling, each with its own configuration format, update cycle, breaking changes, and mental overhead. A fresh " (code "npx create-next-app") " pulls in over 300 packages. The " (code "node_modules") " directory can exceed 200 megabytes. The configuration surface — " (code "webpack.config.js") ", " (code "tsconfig.json") ", " (code ".eslintrc") ", " (code ".prettierrc") ", " (code "postcss.config.js") ", " (code "tailwind.config.js") ", " (code "jest.config.js") ", " (code "next.config.js") " — is itself a specialization.")
(p :class "text-stone-600"
"SX uses " (strong "none of them") "."))
(~doc-section :title "Why each tool is unnecessary" :id "why-unnecessary"
(p :class "text-stone-600"
"Each tool in the conventional stack exists to solve a problem. SX eliminates the problems themselves, not just the tools.")
(h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Bundlers")
(p :class "text-stone-600"
"Bundlers exist because JavaScript's module system requires resolution — imports must be traced, dependencies gathered, and the result packaged for the browser. SX components are defined in " (code ".sx") " files, loaded at startup into the evaluator's environment, and served as s-expressions over the wire. The wire format is the source format. There is no compilation step, no module resolution, and no bundle. The " (code "deps.sx") " spec computes per-page component sets at runtime — only the components a page actually uses are sent to the client. This is tree-shaking without the tree-shaker.")
(h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Transpilers")
(p :class "text-stone-600"
"Transpilers exist because developers write in one language (JSX, TypeScript, modern ES20XX) and must ship another (browser-compatible JavaScript). SX source runs directly — the evaluator walks the same AST that was authored. There is no JSX-to-createElement transform because s-expressions " (em "are") " the component syntax. There are no type annotations to strip because types are enforced at the boundary, at startup. There is no syntax to downlevel because the grammar has been the same since 1958.")
(h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Package managers")
(p :class "text-stone-600"
"Package managers exist because the JavaScript ecosystem fragments functionality into thousands of tiny packages with deep transitive dependency trees. SX's runtime is self-contained: approximately eighty primitives built into the spec, an evaluator bootstrapped from " (code ".sx") " files, and a renderer that produces HTML, DOM nodes, or SX wire format. No third-party packages. No dependency resolution. No lock files. No supply-chain attack surface. The planned future — content-addressed components on IPFS — replaces package registries with content verification.")
(h4 :class "font-semibold text-stone-700 mt-6 mb-2" "CSS build tools")
(p :class "text-stone-600"
"CSS preprocessors and build tools exist because CSS has a global namespace, no scoping mechanism, and no way to know which rules a page actually uses. CSSX solves all three: the server scans the rendered component tree, collects the CSS classes actually referenced, and sends only those rules. Styles are co-located in components as keyword arguments. There is no global stylesheet to collide with, no preprocessor to run, no PurgeCSS to configure. The CSS each page needs is computed from the component graph — a runtime operation, not a build step.")
(h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Dev servers and HMR")
(p :class "text-stone-600"
"Dev servers with hot module replacement exist because compilation creates a feedback delay — you change a file, wait for the bundler to rebuild, and watch the browser update via WebSocket. SX has no compilation. " (code ".sx") " files are checked for changes on every request via " (code "reload_if_changed()") ". Edit the file, refresh the browser, see the result. The feedback loop is bounded by disk I/O, not by a build pipeline. There is no dev server because there is nothing to serve differently from production.")
(h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Linters and formatters")
(p :class "text-stone-600"
"Linters exist because complex grammars permit many ways to write incorrect or inconsistent code. Formatters exist because those same grammars permit many ways to write correct but inconsistently styled code. S-expressions have one syntactic form: " (code "(head args...)") ". Parentheses open and close. Strings are quoted. That is the entire grammar. There are no semicolons to forget, no operator precedence to get wrong, no closing tags to mismatch. Formatting is just indentation of nested lists. There is nothing to lint and nothing to format.")
(h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Type systems")
(p :class "text-stone-600"
"TypeScript exists because JavaScript's dynamic typing defers errors to runtime, often to production. SX's boundary spec declares the signature of every primitive, every IO function, and every page helper. " (code "SX_BOUNDARY_STRICT=1") " validates all registrations at startup — a type violation crashes the application before it serves a single request. Runtime predicates (" (code "string?") ", " (code "number?") ", " (code "list?") ") handle the rest. The type system is not a separate tool running in parallel — it is the spec, enforced at the edge.")
(h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Framework CLIs and scaffolding")
(p :class "text-stone-600"
"Framework CLIs exist because modern frameworks have complex setup — configuration files, directory conventions, build chains, routing configurations. SX has two declarative abstractions: " (code "defcomp") " (a component) and " (code "defpage") " (a route). Write a component in a " (code ".sx") " file, reference it from a " (code "defpage") ", and it is live. There is no scaffolding because there is nothing to scaffold."))
(~doc-section :title "The AI replaces the rest" :id "ai-replaces"
(p :class "text-stone-600"
"Eliminating the build toolchain still leaves the most fundamental tool: the code editor. The text editor is so basic to programming that it is invisible — questioning its necessity sounds absurd. But the traditional editor exists to serve " (em "human") " limitations:")
(ul :class "space-y-2 text-stone-600"
(li (strong "Syntax highlighting") " — because humans need visual cues to parse complex grammars. S-expressions are trivially parseable; an AI does not need color-coding to understand them.")
(li (strong "Autocomplete") " — because humans cannot remember every function name, parameter, and type signature. An AI that has read the entire codebase does not need autocomplete; it already knows every symbol in scope.")
(li (strong "Go-to-definition") " — because humans lose track of where things are defined across hundreds of files. An AI navigates by reading, grep, and glob — the same tools the editor uses internally, without the GUI.")
(li (strong "Error squiggles") " — because humans need real-time feedback on mistakes. An AI produces correct code from the spec, and when it does not, it reads the error, understands the cause, and fixes it in the next edit.")
(li (strong "Multi-cursor editing") " — because humans need mechanical assistance to make the same change in multiple places. An AI makes coordinated changes across files atomically — component definition, navigation data, page route — in a single operation.")
(li (strong "Version control integration") " — because humans need visual diffs and staging interfaces. An AI reads " (code "git diff") ", understands the changes, and commits with meaningful messages."))
(p :class "text-stone-600"
"Every feature of a modern IDE exists to compensate for a human cognitive limitation. When the agent doing the editing has no such limitations — it can hold the full codebase in context, produce valid syntax without visual feedback, trace dependencies without tooling, and make multi-file changes atomically — the editor is not needed.")
(p :class "text-stone-600"
"This is not hypothetical. It is how SX was built. The developer's interface is a terminal running Claude Code. The conversation goes: describe what you want, review what the AI produces, approve or redirect. The AI reads the existing code, understands the conventions, writes the new code, edits the navigation, updates the page routes, and verifies consistency. The " (em "conversation") " is the development environment."))
(~doc-section :title "Before enlightenment, write code" :id "before-enlightenment"
(p :class "text-stone-600"
"Carson Gross makes an important point in " (a :href "https://htmx.org/essays/yes-and/" :class "text-violet-600 hover:underline" "\"Yes, and...\"") ": you have to have written code in order to effectively read code. The ability to review, critique, and direct is built on the experience of having done the work yourself. You cannot skip the craft and jump straight to the oversight.")
(p :class "text-stone-600"
"He is right. " (strong "...and yet."))
(p :class "text-stone-600"
"There is a Zen teaching about the three stages of practice. Before enlightenment: chop wood, carry water. During enlightenment: chop wood, carry water. After enlightenment: chop wood, carry water. The activities look the same from the outside, but the relationship to them has changed completely. The master chops wood without the beginner's anxiety about whether they are chopping correctly, and without the intermediate's self-conscious technique. They just chop.")
(p :class "text-stone-600"
"Software development has its own version. " (em "Before understanding, write code.") " You must have written the bundler configuration to understand why bundlers exist. You must have debugged the CSS cascade to understand why scoping matters. You must have traced a transitive dependency chain to understand why minimal dependencies matter. The craft comes first.")
(p :class "text-stone-600"
(em "During understanding, read code.") " You review pull requests. You audit architectures. You read more code than you write. You develop the taste that lets you tell good code from bad code, necessary complexity from accidental complexity, real problems from invented ones.")
(p :class "text-stone-600"
(em "After understanding, describe intent.") " You have internalized the patterns so deeply that you do not need to type them. You know what a component should look like without writing it character by character. You know what the test should cover without spelling out each assertion. You know what the architecture should be without drawing every box and arrow. You describe; the agent executes. Not because you cannot do the work, but because the work has become transparent to you.")
(p :class "text-stone-600"
"This is not a shortcut. The developer who built SX through conversation with an AI had decades of experience writing code by hand — in Python, VB, C#, JavaScript, C, and others (although he was never that good at it) — but never Lisp. The zero-tooling workflow is not a substitute for that experience. It is what comes " (em "after") " that experience. The wood still gets chopped. The water still gets carried. But the relationship to the chopping and carrying has changed.")
(p :class "text-stone-600"
"What pushed him to use agentic AI was when several of the keys on his keyboard stopped working. Too much coding! AI LLMs don't mind typos."))
(~doc-section :title "Why this only works with s-expressions" :id "why-sexps"
(p :class "text-stone-600"
"This approach would not work with most web technologies. The reason is the " (strong "syntax tax") " — the overhead a language imposes on AI code generation.")
(p :class "text-stone-600"
"Consider what an AI must get right to produce a valid React component: JSX tags that open and close correctly, curly braces for JavaScript expressions inside markup, import statements with correct module paths, semicolons or ASI boundary rules, TypeScript type annotations with angle-bracket generics, CSS-in-JS template literals with different quoting rules from the surrounding code, and hook call ordering constraints that are semantic, not syntactic. Each of these is a failure point. Each failure produces something that does not parse, let alone run.")
(p :class "text-stone-600"
"S-expressions have one rule: parentheses match. The syntax tax is zero. An AI that can count parentheses can produce syntactically valid SX. This means the AI spends its entire capacity on " (em "what") " to generate — the semantics, the structure, the intent — rather than on formatting, escaping, and syntactic bookkeeping.")
(p :class "text-stone-600"
"The combination is more than additive. SX is deliberately spartan for hand-editing — all those parentheses, no syntax sugar, no operator precedence. Developers who see it for the first time recoil. But AI does not recoil. AI does not care about parentheses. It sees a minimal, regular, unambiguous grammar and produces correct output reliably. Meanwhile, the languages that are comfortable for humans — with their rich syntax, implicit rules, and contextual parsing — are exactly the ones where AI makes the most mistakes.")
(p :class "text-stone-600"
"SX is optimized for the agent, not the typist. This turns out to be the right trade-off when the agent is doing the typing."))
(~doc-section :title "What zero-tooling actually means" :id "what-it-means"
(p :class "text-stone-600"
"Zero-tooling does not mean zero software. The SX evaluator exists. The server exists. The browser runtime exists. These are " (em "the system") ", not tools for building the system. The distinction matters.")
(p :class "text-stone-600"
"A carpenter's hammer is a tool. The house is not a tool. In web development, this distinction has been lost. The bundler, the transpiler, the linter, the formatter, the type checker, the dev server — these are all tools for building the application. The application itself is the server, the evaluator, the renderer, the browser runtime. Traditional web development has accumulated so many tools-for-building-the-thing that the tools-for-building-the-thing often have more code, more configuration, and more failure modes than the thing itself.")
(p :class "text-stone-600"
"SX has the thing. It does not have tools for building the thing. You write " (code ".sx") " files. They run. That is the entire workflow.")
(p :class "text-stone-600"
"Add an agentic AI, and you do not even write the " (code ".sx") " files. You describe what you want. The AI writes the files. They run. The workflow is: intent → code → execution, with no intermediate tooling layer and no manual editing step."))
(~doc-section :title "The proof" :id "proof"
(p :class "text-stone-600"
"The evidence for zero-tooling development is not a benchmark or a whitepaper. It is this website.")
(p :class "text-stone-600"
"Every page you are reading was produced through conversation with an agentic AI. The SX evaluator — a self-hosting interpreter with tail-call optimization, delimited continuations, macro expansion, and three rendering backends — was developed without opening a code editor. The specification files that define the language were written without an IDE. The bootstrappers that compile the spec to JavaScript and Python were produced without syntax highlighting or autocomplete. The test suite — hundreds of tests across evaluator, parser, renderer, router, dependency analyzer, and engine — was written without a test runner GUI. This documentation site — with its navigation, its code examples, its live demos — was built without a web development framework's CLI.")
(p :class "text-stone-600"
"The developer sat in a terminal. They described what they wanted. The AI produced the code. When something was wrong, they described what was wrong. The AI fixed it. When something needed to change, they described the change. The AI made the change. Across thousands of files, tens of thousands of lines of code, and months of development. Even the jokes — the " (a :href "/essays/sx-sucks" :class "text-violet-600 hover:underline" "self-deprecating essay") " about everything wrong with SX, the deadpan tone of the documentation, the essay you are reading right now — all produced through conversation, not through typing.")
(p :class "text-stone-600"
"No build step. No bundler. No transpiler. No package manager. No CSS preprocessor. No dev server. No linter. No formatter. No type checker. No framework CLI. No code editor.")
(p :class "text-stone-600"
"Zero tools."))))
;; ---------------------------------------------------------------------------
;; React is Hypermedia
;; ---------------------------------------------------------------------------

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,419 @@
;; ---------------------------------------------------------------------------
;; Content-Addressed Components
;; ---------------------------------------------------------------------------
(defcomp ~plan-content-addressed-components-content ()
(~doc-page :title "Content-Addressed Components"
(~doc-section :title "The Premise" :id "premise"
(p "SX components are pure functions. Boundary enforcement guarantees it — a component cannot call IO primitives, make network requests, access cookies, or touch the filesystem. " (code "Component.is_pure") " is a structural property, verified at registration time by scanning the transitive closure of IO references via " (code "deps.sx") ".")
(p "Pure functions have a remarkable property: " (strong "their identity is their content.") " Two components that produce the same serialized form are the same component, regardless of who wrote them or where they're hosted. This means we can content-address them — compute a cryptographic hash of the canonical serialized form, and that hash " (em "is") " the component's identity.")
(p "Content addressing turns components into shared infrastructure. Define " (code "~card") " once, pin it to IPFS, and every SX application on the planet can use it by CID. No package registry, no npm install, no version conflicts. The CID " (em "is") " the version. The hash " (em "is") " the trust. Boundary enforcement " (em "is") " the sandbox.")
(p "This plan details how to get from the current name-based, per-server component model to a content-addressed, globally-shared one."))
;; -----------------------------------------------------------------------
;; Current State
;; -----------------------------------------------------------------------
(~doc-section :title "Current State" :id "current-state"
(p "What already exists and what's missing.")
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "Capability")
(th :class "px-3 py-2 font-medium text-stone-600" "Status")
(th :class "px-3 py-2 font-medium text-stone-600" "Where")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Deterministic serialization")
(td :class "px-3 py-2 text-stone-700" "Partial — " (code "serialize(body, pretty=True)") " from AST, but no canonical normalization")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "parser.py:296-427"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Component identity")
(td :class "px-3 py-2 text-stone-700" "By name (" (code "~card") ") — names are mutable, server-local")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "types.py:157-180"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Bundle hashing")
(td :class "px-3 py-2 text-stone-700" "SHA256 of all defs concatenated — per-bundle, not per-component")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "jinja_bridge.py:60-86"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Purity verification")
(td :class "px-3 py-2 text-stone-700" (span :class "text-green-700 font-medium" "Complete") " — " (code "is_pure") " via transitive IO ref analysis")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "deps.sx, boundary.py"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Dependency graph")
(td :class "px-3 py-2 text-stone-700" (span :class "text-green-700 font-medium" "Complete") " — " (code "Component.deps") " transitive closure")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "deps.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "IPFS infrastructure")
(td :class "px-3 py-2 text-stone-700" (span :class "text-green-700 font-medium" "Exists") " — IPFSPin model, async upload tasks, CID tracking")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "models/federation.py, artdag/l1/tasks/"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Client component caching")
(td :class "px-3 py-2 text-stone-700" "Hash-based localStorage — but keyed by bundle hash, not individual CID")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "boot.sx, helpers.py"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Content-addressed components")
(td :class "px-3 py-2 text-stone-700" (span :class "text-red-700 font-medium" "Not yet") " — no per-component CID, no IPFS resolution")
(td :class "px-3 py-2 text-stone-600" "—"))))))
;; -----------------------------------------------------------------------
;; Canonical Serialization
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 1: Canonical Serialization" :id "canonical-serialization"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "The foundation")
(p :class "text-violet-800" "Same component must always produce the same bytes, regardless of original formatting, whitespace, or comment placement. Without this, content addressing is meaningless."))
(~doc-subsection :title "The Problem"
(p "Currently " (code "serialize(body, pretty=True)") " produces readable SX source from the parsed AST. But serialization isn't fully canonical — it depends on the internal representation order, and there's no normalization pass. Two semantically identical components formatted differently would produce different hashes.")
(p "We need a " (strong "canonical form") " that strips all variance:"))
(~doc-subsection :title "Canonical Form Rules"
(ol :class "list-decimal pl-5 text-stone-700 space-y-2"
(li (strong "Strip comments.") " Comments are parsing artifacts, not part of the AST. The serializer already ignores them (it works from the parsed tree), but any future comment-preserving parser must not affect canonical output.")
(li (strong "Normalize whitespace.") " Single space between tokens, newline before each top-level form in a body. No trailing whitespace. No blank lines.")
(li (strong "Sort keyword arguments alphabetically.") " In component calls: " (code "(~card :class \"x\" :title \"y\")") " not " (code "(~card :title \"y\" :class \"x\")") ". In dict literals: " (code "{:a 1 :b 2}") " not " (code "{:b 2 :a 1}") ".")
(li (strong "Normalize string escapes.") " Use " (code "\\n") " not literal newlines in strings. Escape only what must be escaped.")
(li (strong "Normalize numbers.") " " (code "1.0") " not " (code "1.00") " or " (code "1.") ". " (code "42") " not " (code "042") ".")
(li (strong "Include the full definition form.") " Hash the complete " (code "(defcomp ~name (params) body)") ", not just the body. The name and parameter signature are part of the component's identity.")))
(~doc-subsection :title "Implementation"
(p "New spec function in a " (code "canonical.sx") " module:")
(~doc-code :code (highlight "(define canonical-serialize\n (fn (node)\n ;; Produce a canonical s-expression string from an AST node.\n ;; Deterministic: same AST always produces same output.\n ;; Used for CID computation — NOT for human-readable output.\n (case (type-of node)\n \"list\"\n (str \"(\" (join \" \" (map canonical-serialize node)) \")\")\n \"dict\"\n (let ((sorted-keys (sort (keys node))))\n (str \"{\" (join \" \"\n (map (fn (k)\n (str \":\" k \" \" (canonical-serialize (get node k))))\n sorted-keys)) \"}\"))\n \"string\"\n (str '\"' (escape-canonical node) '\"')\n \"number\"\n (canonical-number node)\n \"symbol\"\n (symbol-name node)\n \"keyword\"\n (str \":\" (keyword-name node))\n \"boolean\"\n (if node \"true\" \"false\")\n \"nil\"\n \"nil\")))" "lisp"))
(p "This function must be bootstrapped to both Python and JS — the server computes CIDs at registration time, the client verifies them on fetch.")
(p "The canonical serializer is distinct from " (code "serialize()") " for display. " (code "serialize(pretty=True)") " remains for human-readable output. " (code "canonical-serialize") " is for hashing only.")))
;; -----------------------------------------------------------------------
;; CID Computation
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 2: CID Computation" :id "cid-computation"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "Every component gets a stable, unique content identifier. Same source → same CID, always. Different source → different CID, always."))
(~doc-subsection :title "CID Format"
(p "Use " (a :href "https://github.com/multiformats/cid" :class "text-violet-700 underline" "CIDv1") " with:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Hash function:") " SHA3-256 (already used by artdag for content addressing)")
(li (strong "Codec:") " raw (the content is the canonical SX source bytes, not a DAG-PB wrapper)")
(li (strong "Base encoding:") " base32lower for URL-safe representation (" (code "bafy...") " prefix)"))
(~doc-code :code (highlight ";; CID computation pipeline\n(define component-cid\n (fn (component)\n ;; 1. Reconstruct full defcomp form\n ;; 2. Canonical serialize\n ;; 3. SHA3-256 hash\n ;; 4. Wrap as CIDv1\n (let ((source (canonical-serialize\n (list 'defcomp\n (symbol (str \"~\" (component-name component)))\n (component-params-list component)\n (component-body component)))))\n (cid-v1 :sha3-256 :raw (encode-utf8 source)))))" "lisp")))
(~doc-subsection :title "Where CIDs Live"
(p "Each " (code "Component") " object gains a " (code "cid") " field, computed at registration time:")
(~doc-code :code (highlight ";; types.py extension\n@dataclass\nclass Component:\n name: str\n params: list[str]\n has_children: bool\n body: Any\n closure: dict[str, Any]\n css_classes: set[str]\n deps: set[str] # by name\n io_refs: set[str]\n cid: str | None = None # computed after registration\n dep_cids: dict[str, str] | None = None # name → CID" "python"))
(p "After " (code "compute_all_deps()") " runs, a new " (code "compute_all_cids()") " pass fills in CIDs for every component. Dependency CIDs are also recorded — when a component references " (code "~card") ", we store both the name and card's CID."))
(~doc-subsection :title "CID Stability"
(p "A component's CID changes when and only when its " (strong "semantics") " change:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Reformatting the " (code ".sx") " source file → same AST → same canonical form → " (strong "same CID"))
(li "Adding a comment → stripped by parser → same AST → " (strong "same CID"))
(li "Changing a class name in the body → different AST → " (strong "different CID"))
(li "Renaming the component → different defcomp form → " (strong "different CID") " (name is part of identity)"))
(p "This means CIDs are " (em "immutable versions") ". There's no " (code "~card@1.2.3") " — there's " (code "~card") " at CID " (code "bafy...abc") " and " (code "~card") " at CID " (code "bafy...def") ". The name is a human-friendly alias; the CID is the truth.")))
;; -----------------------------------------------------------------------
;; Component Manifest
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 3: Component Manifest" :id "manifest"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "Metadata that travels with a CID — what a component needs, what it provides, whether it's safe to run. Enough information to resolve, validate, and render without fetching the source first."))
(~doc-subsection :title "Manifest Structure"
(~doc-code :code (highlight ";; Component manifest — published alongside the source\n(SxComponent\n :name \"~product-card\"\n :cid \"bafy...productcard\"\n :source-bytes 847\n :params (:title :price :image-url)\n :has-children true\n :pure true\n :deps (\n {:name \"~card\" :cid \"bafy...card\"}\n {:name \"~price-tag\" :cid \"bafy...pricetag\"}\n {:name \"~lazy-image\" :cid \"bafy...lazyimg\"})\n :css-atoms (:border :rounded :p-4 :text-sm :font-bold\n :text-green-700 :line-through :text-stone-400)\n :author \"https://rose-ash.com/apps/market\"\n :published \"2026-03-06T14:30:00Z\")" "lisp"))
(p "Key fields:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code ":cid") " — content address of the canonical serialized source")
(li (code ":deps") " — dependency CIDs, not just names. A consumer can recursively resolve the entire tree by CID without name ambiguity")
(li (code ":pure") " — pre-computed purity flag. The consumer " (em "re-verifies") " this after fetching (never trust the manifest alone), but it enables fast rejection of IO-dependent components before downloading")
(li (code ":deps") " includes style component CIDs. No separate " (code ":css-atoms") " field needed — styling is just more components")
(li (code ":params") " — parameter signature for tooling, documentation, IDE support")
(li (code ":author") " — who published this. AP actor URL, verifiable via HTTP Signatures")))
(~doc-subsection :title "Manifest CID"
(p "The manifest itself is content-addressed. But the manifest CID is " (em "not") " the component CID — they're separate objects. The component CID is derived from the source alone (pure content). The manifest CID includes metadata that could change (author, publication date) without changing the component.")
(p "Resolution order: manifest CID → manifest → component CID → component source. Or shortcut: component CID → source directly, if you already know what you need.")))
;; -----------------------------------------------------------------------
;; IPFS Storage & Resolution
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 4: IPFS Storage & Resolution" :id "ipfs"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "Components live on IPFS. Any browser can fetch them by CID. No origin server needed. No CDN. No DNS. The content network IS the distribution network."))
(~doc-subsection :title "Server-Side: Publication"
(p "On component registration (startup or hot-reload), the server:")
(ol :class "list-decimal pl-5 text-stone-700 space-y-1"
(li "Computes canonical form and CID")
(li "Checks " (code "IPFSPin") " — if CID already pinned, skip (content can't have changed)")
(li "Pins canonical source to IPFS (async Celery task, same pattern as artdag)")
(li "Creates/updates " (code "IPFSPin") " record with " (code "pin_type=\"component\""))
(li "Publishes manifest to IPFS (separate CID)")
(li "Optionally announces via AP outbox for federated discovery"))
(~doc-code :code (highlight ";; IPFSPin usage for components\nIPFSPin(\n content_hash=\"sha3-256:abcdef...\",\n ipfs_cid=\"bafy...productcard\",\n pin_type=\"component\",\n source_type=\"market\", # which service defined it\n metadata={\n \"name\": \"~product-card\",\n \"manifest_cid\": \"bafy...manifest\",\n \"deps\": [\"bafy...card\", \"bafy...pricetag\"],\n \"pure\": True\n }\n)" "python")))
(~doc-subsection :title "Client-Side: Resolution"
(p "New spec module " (code "resolve.sx") " — the client-side component resolution pipeline:")
(~doc-code :code (highlight "(define resolve-component-by-cid\n (fn (cid callback)\n ;; Resolution cascade:\n ;; 1. Check component env (already loaded?)\n ;; 2. Check localStorage (keyed by CID = cache-forever)\n ;; 3. Check origin server (/sx/components?cid=bafy...)\n ;; 4. Fetch from IPFS gateway\n ;; 5. Verify hash matches CID\n ;; 6. Parse, validate purity, register, callback\n (let ((cached (local-storage-get (str \"sx-cid:\" cid))))\n (if cached\n (do\n (register-component-source cached)\n (callback true))\n (fetch-component-by-cid cid\n (fn (source)\n (if (verify-cid cid source)\n (do\n (local-storage-set (str \"sx-cid:\" cid) source)\n (register-component-source source)\n (callback true))\n (do\n (log-warn (str \"sx:cid verification failed \" cid))\n (callback false)))))))))" "lisp"))
(p "The cache-forever semantics are the key insight: because CIDs are content-addressed, a cached component " (strong "can never be stale") ". If the source changes, it gets a new CID. Old CIDs remain valid forever. There is no cache invalidation problem."))
(~doc-subsection :title "Resolution Cascade"
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "Layer")
(th :class "px-3 py-2 font-medium text-stone-600" "Lookup")
(th :class "px-3 py-2 font-medium text-stone-600" "Latency")
(th :class "px-3 py-2 font-medium text-stone-600" "When")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "1. Component env")
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "(env-has? env cid)")
(td :class "px-3 py-2 text-stone-600" "0ms")
(td :class "px-3 py-2 text-stone-600" "Already loaded this session"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "2. localStorage")
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "localStorage[\"sx-cid:\" + cid]")
(td :class "px-3 py-2 text-stone-600" "<1ms")
(td :class "px-3 py-2 text-stone-600" "Previously fetched, persists across sessions"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "3. Origin server")
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "GET /sx/components?cid=bafy...")
(td :class "px-3 py-2 text-stone-600" "~20ms")
(td :class "px-3 py-2 text-stone-600" "Same-origin component, not yet cached"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "4. IPFS gateway")
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "GET https://gateway/ipfs/{cid}")
(td :class "px-3 py-2 text-stone-600" "~200ms")
(td :class "px-3 py-2 text-stone-600" "Foreign component, federated content"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "5. Local IPFS node")
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "ipfs cat {cid}")
(td :class "px-3 py-2 text-stone-600" "~5ms")
(td :class "px-3 py-2 text-stone-600" "User runs own IPFS node (power users)")))))
(p "Layer 5 is optional — checked between 2 and 3 if " (code "window.ipfs") " or a local gateway is detected. For most users, layers 1-4 cover all cases.")))
;; -----------------------------------------------------------------------
;; Security Model
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 5: Security Model" :id "security"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "The hard part")
(p :class "text-violet-800" "Loading code from the network is the web's original sin. Content-addressed components are safe because of three structural guarantees — not policies, not trust, not sandboxes that can be escaped."))
(~doc-subsection :title "Guarantee 1: Purity is Structural"
(p "SX boundary enforcement isn't a runtime sandbox — it's a registration-time structural check. When a component is loaded from IPFS and parsed, " (code "compute_all_io_refs()") " walks its entire AST and transitive dependencies. If " (em "any") " node references an IO primitive, the component is classified as IO-dependent and " (strong "rejected for untrusted registration."))
(p "This means the evaluator literally doesn't have IO primitives in scope when running an IPFS-loaded component. It's not that we catch IO calls — the names don't resolve. There's nothing to catch.")
(~doc-code :code (highlight "(define register-untrusted-component\n (fn (source origin)\n ;; Parse the defcomp from source\n ;; Run compute-all-io-refs on the parsed component\n ;; If io_refs is non-empty → REJECT\n ;; If pure → register in env with :origin metadata\n (let ((comp (parse-component source)))\n (if (not (component-pure? comp))\n (do\n (log-warn (str \"sx:reject IO component from \" origin))\n nil)\n (do\n (register-component comp)\n (log-info (str \"sx:registered \" (component-name comp)\n \" from \" origin))\n comp)))))" "lisp")))
(~doc-subsection :title "Guarantee 2: Content Verification"
(p "The CID IS the hash. When you fetch " (code "bafy...abc") " from any source — IPFS gateway, origin server, peer — you hash the response and compare. If it doesn't match, you reject it. No MITM attack can alter the content without changing the CID.")
(p "This is stronger than HTTPS. HTTPS trusts the certificate authority, the DNS resolver, and the server operator. Content addressing trusts " (em "mathematics") ". The hash either matches or it doesn't."))
(~doc-subsection :title "Guarantee 3: Evaluation Limits"
(p "Pure doesn't mean terminating. A component could contain an infinite loop or exponential recursion. SX evaluators enforce step limits:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Max eval steps:") " configurable per context. Untrusted components get a lower limit than local ones.")
(li (strong "Max recursion depth:") " prevents stack exhaustion.")
(li (strong "Max output size:") " prevents a component from producing gigabytes of DOM nodes."))
(p "Exceeding any limit halts evaluation and returns an error node. The worst case is wasted CPU — never data exfiltration, never unauthorized IO."))
(~doc-subsection :title "Trust Tiers"
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "Tier")
(th :class "px-3 py-2 font-medium text-stone-600" "Source")
(th :class "px-3 py-2 font-medium text-stone-600" "Allowed")
(th :class "px-3 py-2 font-medium text-stone-600" "Eval limits")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Local")
(td :class "px-3 py-2 text-stone-700" "Server's own " (code ".sx") " files")
(td :class "px-3 py-2 text-stone-700" "Pure + IO primitives + page helpers")
(td :class "px-3 py-2 text-stone-600" "None (trusted)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Followed")
(td :class "px-3 py-2 text-stone-700" "Components from followed AP actors")
(td :class "px-3 py-2 text-stone-700" "Pure only (IO rejected)")
(td :class "px-3 py-2 text-stone-600" "Standard limits"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Federated")
(td :class "px-3 py-2 text-stone-700" "Components from any IPFS source")
(td :class "px-3 py-2 text-stone-700" "Pure only (IO rejected)")
(td :class "px-3 py-2 text-stone-600" "Strict limits"))))))
(~doc-subsection :title "What Can Go Wrong"
(p "Honest accounting of the attack surface:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Visual spoofing:") " A malicious component could render UI that looks like a login form. Mitigation: untrusted components render inside a visually distinct container with origin attribution.")
(li (strong "CSS abuse:") " A component's CSS atoms could interfere with page layout. Mitigation: scoped CSS — untrusted components' classes are namespaced.")
(li (strong "Resource exhaustion:") " A component could be expensive to evaluate. Mitigation: step limits, timeout, lazy rendering for off-screen components.")
(li (strong "Privacy leak via CSS:") " Background-image URLs could phone home. Mitigation: CSP restrictions on untrusted component rendering contexts.")
(li (strong "Dependency confusion:") " A malicious manifest could claim deps that are different components with the same name. Mitigation: deps are referenced by CID, not name. Name is informational only."))))
;; -----------------------------------------------------------------------
;; Wire Format & Prefetch Integration
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 6: Wire Format & Prefetch Integration" :id "wire-format"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "Pages and SX responses reference components by CID. The prefetch system resolves them from the most efficient source. Components become location-independent."))
(~doc-subsection :title "CID References in Page Registry"
(p "The page registry (shipped to the client as " (code "<script type=\"text/sx-pages\">") ") currently lists deps by name. Extend to include CIDs:")
(~doc-code :code (highlight "{:name \"docs-page\" :path \"/docs/<slug>\"\n :auth \"public\" :has-data false\n :deps ({:name \"~essay-foo\" :cid \"bafy...essay\"}\n {:name \"~doc-code\" :cid \"bafy...doccode\"})\n :content \"(case slug ...)\" :closure {}}" "lisp"))
(p "The " (a :href "/plans/predictive-prefetch" :class "text-violet-700 underline" "predictive prefetch system") " uses these CIDs to fetch components from the resolution cascade rather than only from the origin server's " (code "/sx/components") " endpoint."))
(~doc-subsection :title "SX Response Component Headers"
(p "Currently, " (code "SX-Components") " header lists loaded component names. Extend to support CIDs:")
(~doc-code :code (highlight "Request:\nSX-Components: ~card:bafy...card,~nav:bafy...nav\n\nResponse:\nSX-Component-CIDs: ~essay-foo:bafy...essay,~doc-code:bafy...doccode\n\n;; Response body only includes defs the client doesn't have\n(defcomp ~essay-foo ...)" "http"))
(p "The client can then verify received components match their declared CIDs. If the origin server is compromised, CID verification catches the tampered response."))
(~doc-subsection :title "Federated Content"
(p "When an ActivityPub activity arrives with SX content, it declares component requirements by CID:")
(~doc-code :code (highlight "(Create\n :actor \"https://other-instance.com/users/bob\"\n :object (Note\n :content (~product-card :title \"Bob's Widget\" :price 29.99)\n :requires (list\n {:name \"~product-card\" :cid \"bafy...prodcard\"}\n {:name \"~price-tag\" :cid \"bafy...pricetag\"})))" "lisp"))
(p "The receiving browser resolves required components through the cascade. If Bob's instance is down, the components are still fetchable from IPFS. The content is self-describing and self-resolving.")))
;; -----------------------------------------------------------------------
;; Component Sharing & Discovery
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 7: Sharing & Discovery" :id "sharing"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "Servers publish component collections via AP. Other servers follow them. Like npm, but federated, content-addressed, and structurally safe."))
(~doc-subsection :title "Component Registry as AP Actor"
(p "Each server exposes a component registry actor:")
(~doc-code :code (highlight "(Service\n :id \"https://rose-ash.com/sx-registry\"\n :type \"SxComponentRegistry\"\n :name \"Rose Ash Components\"\n :outbox \"https://rose-ash.com/sx-registry/outbox\"\n :followers \"https://rose-ash.com/sx-registry/followers\")" "lisp"))
(p "Follow the registry to receive component updates. The outbox is a chronological feed of Create/Update/Delete activities for components. 'Update' means a new CID for the same name — consumers decide whether to adopt it."))
(~doc-subsection :title "Discovery Protocol"
(p "Webfinger-style lookup for components by name:")
(~doc-code :code (highlight "GET /.well-known/sx-component?name=~product-card\n\n{\n \"name\": \"~product-card\",\n \"cid\": \"bafy...prodcard\",\n \"manifest_cid\": \"bafy...manifest\",\n \"gateway\": \"https://rose-ash.com/ipfs/\",\n \"author\": \"https://rose-ash.com/apps/market\"\n}" "http"))
(p "This is an optional convenience — any consumer that knows the CID can skip discovery and fetch directly from IPFS. Discovery answers the question: " (em "\"what's the current version of ~product-card on rose-ash.com?\""))
)
(~doc-subsection :title "Name Resolution"
(p "Names are human-friendly aliases for CIDs. The same name on different servers can refer to different components (different CIDs). Conflict resolution is simple:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Local wins:") " If the server defines " (code "~card") ", that definition takes precedence over any federated " (code "~card") ".")
(li (strong "CID pinning:") " When referencing a federated component, pin the CID. " (code "(:name \"~card\" :cid \"bafy...abc\")") " — the name is informational, the CID is authoritative.")
(li (strong "No global namespace:") " There is no \"npm\" that owns " (code "~card") ". Names are scoped to the server that defines them. CIDs are global."))))
;; -----------------------------------------------------------------------
;; Spec modules
;; -----------------------------------------------------------------------
(~doc-section :title "Spec Modules" :id "spec-modules"
(p "Per the SX host architecture principle, all content-addressing logic is specced in " (code ".sx") " files and bootstrapped:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "Spec module")
(th :class "px-3 py-2 font-medium text-stone-600" "Functions")
(th :class "px-3 py-2 font-medium text-stone-600" "Platform obligations")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "canonical.sx")
(td :class "px-3 py-2 text-stone-700" (code "canonical-serialize") ", " (code "canonical-number") ", " (code "escape-canonical"))
(td :class "px-3 py-2 text-stone-600" "None — pure string operations"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "cid.sx")
(td :class "px-3 py-2 text-stone-700" (code "component-cid") ", " (code "verify-cid") ", " (code "cid-to-string") ", " (code "parse-cid"))
(td :class "px-3 py-2 text-stone-600" (code "sha3-256") ", " (code "encode-base32") ", " (code "encode-utf8")))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "resolve.sx")
(td :class "px-3 py-2 text-stone-700" (code "resolve-component-by-cid") ", " (code "resolve-deps-recursive") ", " (code "register-untrusted-component"))
(td :class "px-3 py-2 text-stone-600" (code "local-storage-get/set") ", " (code "fetch-cid") ", " (code "register-component-source"))))))
;; -----------------------------------------------------------------------
;; Critical files
;; -----------------------------------------------------------------------
(~doc-section :title "Critical Files" :id "critical-files"
(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")
(th :class "px-3 py-2 font-medium text-stone-600" "Phase")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/canonical.sx")
(td :class "px-3 py-2 text-stone-700" "Canonical serialization spec (new)")
(td :class "px-3 py-2 text-stone-600" "1"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/cid.sx")
(td :class "px-3 py-2 text-stone-700" "CID computation and verification spec (new)")
(td :class "px-3 py-2 text-stone-600" "2"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/types.py")
(td :class "px-3 py-2 text-stone-700" "Add " (code "cid") " and " (code "dep_cids") " to Component")
(td :class "px-3 py-2 text-stone-600" "2"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/jinja_bridge.py")
(td :class "px-3 py-2 text-stone-700" "Add " (code "compute_all_cids()") " to registration lifecycle")
(td :class "px-3 py-2 text-stone-600" "2"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/models/federation.py")
(td :class "px-3 py-2 text-stone-700" "IPFSPin records for component CIDs")
(td :class "px-3 py-2 text-stone-600" "4"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/resolve.sx")
(td :class "px-3 py-2 text-stone-700" "Client-side CID resolution cascade (new)")
(td :class "px-3 py-2 text-stone-600" "4"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/helpers.py")
(td :class "px-3 py-2 text-stone-700" "CIDs in page registry, " (code "/sx/components?cid=") " endpoint")
(td :class "px-3 py-2 text-stone-600" "6"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/orchestration.sx")
(td :class "px-3 py-2 text-stone-700" "CID-aware prefetch in resolution cascade")
(td :class "px-3 py-2 text-stone-600" "6"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/infrastructure/activitypub.py")
(td :class "px-3 py-2 text-stone-700" "Component registry actor, Webfinger extension")
(td :class "px-3 py-2 text-stone-600" "7"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/boundary.py")
(td :class "px-3 py-2 text-stone-700" "Trust tier enforcement for untrusted components")
(td :class "px-3 py-2 text-stone-600" "5"))))))
;; -----------------------------------------------------------------------
;; Relationship
;; -----------------------------------------------------------------------
(~doc-section :title "Relationships" :id "relationships"
(p "This plan is the foundation for several other plans and roadmaps:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (a :href "/plans/sx-activity" :class "text-violet-700 underline" "SX-Activity") " Phase 2 (content-addressed components on IPFS) is a summary of this plan. This plan supersedes that section with full detail.")
(li (a :href "/plans/predictive-prefetch" :class "text-violet-700 underline" "Predictive prefetching") " gains CID-based resolution — the " (code "/sx/components") " endpoint and IPFS gateway become alternative resolution paths in the prefetch cascade.")
(li (a :href "/plans/isomorphic-architecture" :class "text-violet-700 underline" "Isomorphic architecture") " Phase 1 (component distribution) is enhanced — CIDs make per-page bundles verifiable and cross-server shareable.")
(li "The SX-Activity vision of " (strong "serverless applications on IPFS") " depends entirely on this plan. Without content-addressed components, applications can't be pinned to IPFS as self-contained artifacts."))
(div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2"
(p :class "text-amber-800 text-sm" (strong "Depends on: ") "deps.sx (complete), boundary enforcement (complete), IPFS infrastructure (exists in artdag, needs wiring to web platform)."))))))
;; ---------------------------------------------------------------------------
;; Predictive Component Prefetching
;; ---------------------------------------------------------------------------

View File

@@ -0,0 +1,51 @@
;; ---------------------------------------------------------------------------
;; Fragment Protocol
;; ---------------------------------------------------------------------------
(defcomp ~plan-fragment-protocol-content ()
(~doc-page :title "Fragment Protocol"
(~doc-section :title "Context" :id "context"
(p "Fragment endpoints return raw sexp source (e.g., " (code "(~blog-nav-wrapper :items ...)") "). The consuming service embeds this in its page sexp, which the client evaluates. But service-specific components like " (code "~blog-nav-wrapper") " are only in that service's component env — not in the consumer's. So the consumer's " (code "client_components_tag()") " never sends them to the client, causing \"Unknown component\" errors.")
(p "The fix: transfer component definitions alongside fragments. Services tell the provider what they already have; the provider sends only what's missing."))
(~doc-section :title "What exists" :id "exists"
(div :class "rounded border border-green-200 bg-green-50 p-4"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Fragment GET infrastructure works (" (code "shared/infrastructure/fragments.py") ")")
(li (code "X-Fragment-Request") " header protocol for internal service calls")
(li "Content type negotiation for text/html and text/sx responses")
(li "Fragment caching and composition in page rendering"))))
(~doc-section :title "What remains" :id "remains"
(div :class "rounded border border-amber-200 bg-amber-50 p-4"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "POST sexp protocol: ") "Switch from GET to POST with structured sexp body containing " (code ":components") " list of what consumer already has")
(li (strong "Structured response: ") (code "(fragment-response :defs (...) :content (...))") " — provider sends only missing component defs")
(li (strong (code "fragment_response()") " builder: ") "New function in helpers.py that diffs provider's component env against consumer's list")
(li (strong "Register received defs: ") "Consumer parses " (code ":defs") " from response and registers into its " (code "_COMPONENT_ENV"))
(li (strong "Shared blueprint factory: ") (code "create_fragment_blueprint(handlers)") " to deduplicate the identical fragment endpoint pattern across 8 services"))))
(~doc-section :title "Files to modify" :id "files"
(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" "Change")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/infrastructure/fragments.py")
(td :class "px-3 py-2 text-stone-700" "POST sexp body, parse response, register defs"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/helpers.py")
(td :class "px-3 py-2 text-stone-700" "fragment_response() builder"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/infrastructure/fragment_endpoint.py")
(td :class "px-3 py-2 text-stone-700" "NEW — shared blueprint factory"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "*/bp/fragments/routes.py")
(td :class "px-3 py-2 text-stone-700" "All 8 services: use create_fragment_blueprint"))))))))
;; ---------------------------------------------------------------------------
;; Glue Decoupling
;; ---------------------------------------------------------------------------

View File

@@ -0,0 +1,49 @@
;; ---------------------------------------------------------------------------
;; Glue Decoupling
;; ---------------------------------------------------------------------------
(defcomp ~plan-glue-decoupling-content ()
(~doc-page :title "Cross-App Decoupling via Glue Services"
(~doc-section :title "Context" :id "context"
(p "All cross-domain FK constraints have been dropped (with pragmatic exceptions for OrderItem.product_id and CartItem). Cross-domain writes go through internal HTTP and activity bus. However, " (strong "25+ cross-app model imports remain") " — apps still import from each other's models/ directories. This means every app needs every other app's code on disk to start.")
(p "The goal: eliminate all cross-app model imports. Every app only imports from its own models/, from shared/, and from a new glue/ service layer."))
(~doc-section :title "Current state" :id "current"
(p "Apps are partially decoupled via HTTP interfaces (fetch_data, call_action, send_internal_activity) and DTOs. The Cart microservice split (relations, likes, orders) is complete. But direct model imports persist in:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Cart") " — 9 files importing from market, events, blog")
(li (strong "Blog") " — 8 files importing from cart, events, market")
(li (strong "Events") " — 5 files importing from blog, market, cart")
(li (strong "Market") " — 1 file importing from blog")))
(~doc-section :title "What remains" :id "remains"
(div :class "space-y-3"
(div :class "rounded border border-stone-200 p-3"
(h4 :class "font-semibold text-stone-700" "1. glue/services/pages.py")
(p :class "text-sm text-stone-600" "Dict-based Post access for non-blog apps: get_page_by_slug, get_page_by_id, get_pages_by_ids, page_exists, search_posts."))
(div :class "rounded border border-stone-200 p-3"
(h4 :class "font-semibold text-stone-700" "2. glue/services/page_config.py")
(p :class "text-sm text-stone-600" "PageConfig CRUD: get_page_config, get_or_create_page_config, get_page_configs_by_ids."))
(div :class "rounded border border-stone-200 p-3"
(h4 :class "font-semibold text-stone-700" "3. glue/services/calendars.py")
(p :class "text-sm text-stone-600" "Calendar queries + entry associations (from blog): get_calendars_for_page, toggle_entry_association, get_associated_entries."))
(div :class "rounded border border-stone-200 p-3"
(h4 :class "font-semibold text-stone-700" "4. glue/services/marketplaces.py")
(p :class "text-sm text-stone-600" "MarketPlace CRUD (from blog+events): get_marketplaces_for_page, create_marketplace, soft_delete_marketplace."))
(div :class "rounded border border-stone-200 p-3"
(h4 :class "font-semibold text-stone-700" "5. glue/services/cart_items.py")
(p :class "text-sm text-stone-600" "CartItem/CalendarEntry queries for cart: get_cart_items, find_or_create_cart_item, clear_cart_for_order."))
(div :class "rounded border border-stone-200 p-3"
(h4 :class "font-semibold text-stone-700" "6. glue/services/products.py")
(p :class "text-sm text-stone-600" "Minimal Product access for cart orders: get_product."))
(div :class "rounded border border-stone-200 p-3"
(h4 :class "font-semibold text-stone-700" "7. Model registration + cleanup")
(p :class "text-sm text-stone-600" "register_models() in glue/setup.py, update all app.py files, delete moved service files."))))
(~doc-section :title "Docker consideration" :id "docker"
(p :class "text-stone-600" "For glue services to work in Docker (single app per container), model files from other apps must be importable. Recommended: try/except at import time — glue services that can't import a model raise ImportError at call time, which only happens if called from the wrong app."))))
;; ---------------------------------------------------------------------------
;; Social Sharing
;; ---------------------------------------------------------------------------

25
sx/sx/plans/index.sx Normal file
View File

@@ -0,0 +1,25 @@
;; Plans section — architecture roadmaps and implementation plans
;; ---------------------------------------------------------------------------
;; Plans index page
;; ---------------------------------------------------------------------------
(defcomp ~plans-index-content ()
(~doc-page :title "Plans"
(div :class "space-y-4"
(p :class "text-lg text-stone-600 mb-4"
"Architecture roadmaps and implementation plans for SX.")
(div :class "space-y-3"
(map (fn (item)
(a :href (get item "href")
:sx-get (get item "href") :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
:class "block rounded border border-stone-200 p-4 hover:border-violet-300 hover:bg-violet-50 transition-colors"
(div :class "font-semibold text-stone-800" (get item "label"))
(when (get item "summary")
(p :class "text-sm text-stone-500 mt-1" (get item "summary")))))
plans-nav-items)))))
;; ---------------------------------------------------------------------------
;; Reader Macros
;; ---------------------------------------------------------------------------

682
sx/sx/plans/isomorphic.sx Normal file
View File

@@ -0,0 +1,682 @@
;; ---------------------------------------------------------------------------
;; Isomorphic Architecture Roadmap
;; ---------------------------------------------------------------------------
(defcomp ~plan-isomorphic-content ()
(~doc-page :title "Isomorphic Architecture Roadmap"
(~doc-section :title "Context" :id "context"
(p "SX has a working server-client pipeline: server evaluates pages with IO (DB, fragments), serializes as SX wire format, client parses and renders to DOM. The language and primitives are already isomorphic " (em "— same spec, same semantics, both sides.") " What's missing is the " (strong "plumbing") " that makes the boundary between server and client a sliding window rather than a fixed wall.")
(p "The key insight: " (strong "s-expressions can partially unfold on the server after IO, then finish unfolding on the client.") " The system knows which components have data fetches (via IO detection in " (a :href "/specs/deps" :class "text-violet-700 underline" "deps.sx") "), resolves those server-side, and sends the rest as pure SX for client rendering. The boundary slides automatically based on what each component actually needs."))
(~doc-section :title "Current State" :id "current-state"
(ul :class "space-y-2 text-stone-700 list-disc pl-5"
(li (strong "Primitive parity: ") "100%. ~80 pure primitives, same names/semantics, JS and Python.")
(li (strong "eval/parse/render: ") "Complete both sides. sx-ref.js has eval, parse, render-to-html, render-to-dom, aser.")
(li (strong "Engine: ") "engine.sx (morph, swaps, triggers, history), orchestration.sx (fetch, events), boot.sx (hydration) — all transpiled.")
(li (strong "Wire format: ") "Server _aser → SX source → client parses → renders to DOM. Boundary is clean.")
(li (strong "Component caching: ") "Hash-based localStorage for component definitions.")
(li (strong "Boundary enforcement: ") "boundary.sx + SX_BOUNDARY_STRICT=1 validates all primitives/IO/helpers at registration.")
(li (strong "Dependency analysis: ") "deps.sx computes per-page component bundles — only definitions a page actually uses are sent.")
(li (strong "IO detection: ") "deps.sx classifies every component as pure or IO-dependent. Server expands IO components, serializes pure ones for client.")
(li (strong "Client-side routing: ") "router.sx matches URL patterns. Pure pages render instantly without server roundtrips. Pages with :data fall through to server transparently.")
(li (strong "Client IO proxy: ") "IO primitives registered on the client call back to the server via fetch. Components with IO deps can render client-side.")
(li (strong "Streaming/suspense: ") "defpage :stream true enables chunked HTML. ~suspense placeholders show loading skeletons; __sxResolve() fills in content as IO completes.")))
;; -----------------------------------------------------------------------
;; Phase 1
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 1: Component Distribution & Dependency Analysis" :id "phase-1"
(div :class "rounded border border-green-300 bg-green-50 p-4 mb-4"
(div :class "flex items-center gap-2 mb-2"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
(a :href "/specs/deps" :class "text-green-700 underline text-sm font-medium" "View canonical spec: deps.sx")
(a :href "/isomorphism/bundle-analyzer" :class "text-green-700 underline text-sm font-medium" "Live bundle analyzer"))
(p :class "text-green-900 font-medium" "What it enables")
(p :class "text-green-800" "Per-page component bundles instead of sending every definition to every page. Smaller payloads, faster boot, better cache hit rates."))
(~doc-subsection :title "The Problem"
(p "The page boot payload serializes every component definition in the environment. A page that uses 5 components still receives all 50+. No mechanism determines which components a page actually needs — the boundary between \"loaded\" and \"used\" is invisible."))
(~doc-subsection :title "Implementation"
(p "The dependency analysis algorithm is defined in "
(a :href "/specs/deps" :class "text-violet-700 underline" "deps.sx")
" — a spec module bootstrapped to every host. Each host loads it via " (code "--spec-modules deps") " and provides 6 platform functions. The spec is the single source of truth; hosts are interchangeable.")
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Transitive closure (deps.sx)")
(p "9 functions that walk the component graph. The core:")
(~doc-code :code (highlight "(define (transitive-deps name env)\n (let ((key (if (starts-with? name \"~\") name\n (concat \"~\" name)))\n (seen (set-create)))\n (transitive-deps-walk key env seen)\n (set-remove seen key)))" "lisp"))
(p (code "scan-refs") " walks a component body AST collecting " (code "~") " symbols. "
(code "transitive-deps") " follows references recursively through the env. "
(code "compute-all-deps") " batch-computes and caches deps for every component. "
"Circular references terminate safely via a seen-set."))
(div
(h4 :class "font-semibold text-stone-700" "2. Page scanning")
(~doc-code :code (highlight "(define (components-needed page-source env)\n (let ((direct (scan-components-from-source page-source))\n (all-needed (set-create)))\n (for-each (fn (name) ...\n (set-add! all-needed name)\n (set-union! all-needed (component-deps comp)))\n direct)\n all-needed))" "lisp"))
(p (code "scan-components-from-source") " finds " (code "(~name") " patterns in serialized SX via regex. " (code "components-needed") " combines scanning with the cached transitive closure to produce the minimal component set for a page."))
(div
(h4 :class "font-semibold text-stone-700" "3. Per-page CSS scoping")
(p (code "page-css-classes") " unions the CSS classes from only the components in the page bundle. Pages that don't use a component never pay for its styles."))
(div
(h4 :class "font-semibold text-stone-700" "4. Platform interface")
(p "The spec declares 6 functions each host implements natively — the only host-specific code:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "component-deps") " / " (code "component-set-deps!") " — read/write the cached deps set on a component object")
(li (code "component-css-classes") " — read pre-scanned CSS class names from a component")
(li (code "env-components") " — enumerate all component entries in an environment")
(li (code "regex-find-all") " / " (code "scan-css-classes") " — host-native regex and CSS scanning")))))
(~doc-subsection :title "Spec module"
(p "deps.sx is loaded as a " (strong "spec module") " — an optional extension to the core spec. The bootstrapper flag " (code "--spec-modules deps") " includes it in the generated output alongside the core evaluator, parser, and renderer. Phase 2 IO detection was added to the same module — same bootstrapping mechanism, no architecture changes needed.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "shared/sx/ref/deps.sx — canonical spec (14 functions, 8 platform declarations)")
(li "Bootstrapped to all host targets via --spec-modules deps")))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "15 dedicated tests: scan, transitive closure, circular deps, compute-all, components-needed")
(li "Bootstrapped output verified on both host targets")
(li "Full test suite passes with zero regressions")
(li (a :href "/isomorphism/bundle-analyzer" :class "text-violet-700 underline" "Live bundle analyzer") " shows real per-page savings on this app"))))
;; -----------------------------------------------------------------------
;; Phase 2
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 2: Smart Server/Client Boundary — IO Detection" :id "phase-2"
(div :class "rounded border border-green-300 bg-green-50 p-4 mb-4"
(div :class "flex items-center gap-2 mb-2"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
(a :href "/specs/deps" :class "text-green-700 underline text-sm font-medium" "View canonical spec: deps.sx")
(a :href "/isomorphism/bundle-analyzer" :class "text-green-700 underline text-sm font-medium" "Live bundle analyzer with IO"))
(p :class "text-green-900 font-medium" "What it enables")
(p :class "text-green-800" "Automatic IO detection and selective expansion. Server expands IO-dependent components, serializes pure ones for client. Per-component intelligence replaces global toggle."))
(~doc-subsection :title "IO Detection in the Spec"
(p "Five new functions in "
(a :href "/specs/deps" :class "text-violet-700 underline" "deps.sx")
" extend the Phase 1 walker to detect IO primitive references:")
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. IO scanning")
(p (code "scan-io-refs") " walks an AST node, collecting symbol names that match an IO name set. The IO set is provided by the host from boundary declarations (all three tiers: core IO, deployment IO, page helpers).")
(~doc-code :code (highlight "(define scan-io-refs\n (fn (node io-names)\n (let ((refs (list)))\n (scan-io-refs-walk node io-names refs)\n refs)))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "2. Transitive IO closure")
(p (code "transitive-io-refs") " follows component deps recursively, unioning IO refs from all reachable components and macros. Cycle-safe via seen-set.")
(~doc-code :code (highlight "(define transitive-io-refs\n (fn (name env io-names)\n ;; Walk deps, scan each body for IO refs,\n ;; union all refs transitively.\n ...))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "3. Batch computation")
(p (code "compute-all-io-refs") " iterates the env, computes transitive IO refs for each component, and caches the result via " (code "component-set-io-refs!") ". Called after " (code "compute-all-deps") " at component registration time."))
(div
(h4 :class "font-semibold text-stone-700" "4. Component metadata")
(p "Each component now carries " (code "io_refs") " (transitive IO primitive names) alongside " (code "deps") " and " (code "css_classes") ". The derived " (code "is_pure") " property is true when " (code "io_refs") " is empty — the component can render anywhere without server data."))))
(~doc-subsection :title "Selective Expansion"
(p "The partial evaluator " (code "_aser") " now uses per-component IO metadata instead of a global toggle:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "IO-dependent") " → expand server-side (IO must resolve)")
(li (strong "Pure") " → serialize for client (let client render)")
(li (strong "Layout slot context") " → all components still expand (backwards compat via " (code "_expand_components") " context var)"))
(p "A component calling " (code "(highlight ...)") " or " (code "(query ...)") " is IO-dependent. A component with only HTML tags and string ops is pure."))
(~doc-subsection :title "Platform interface additions"
(p "Two new platform functions each host implements:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "(component-io-refs c) → cached IO ref list")
(li "(component-set-io-refs! c refs) → cache IO refs on component")))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Components calling (query ...) or (highlight ...) classified IO-dependent")
(li "Pure components (HTML-only) classified pure with empty io_refs")
(li "Transitive IO detection: component calling ~other where ~other calls (current-user) → IO-dependent")
(li "Bootstrapped to both hosts (sx_ref.py + sx-ref.js)")
(li (a :href "/isomorphism/bundle-analyzer" :class "text-violet-700 underline" "Live bundle analyzer") " shows per-page IO classification"))))
;; -----------------------------------------------------------------------
;; Phase 3
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 3: Client-Side Routing (SPA Mode)" :id "phase-3"
(div :class "rounded border border-green-300 bg-green-50 p-4 mb-4"
(div :class "flex items-center gap-2 mb-2"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
(a :href "/specs/router" :class "text-green-700 underline text-sm font-medium" "View canonical spec: router.sx")
(a :href "/isomorphism/routing-analyzer" :class "text-green-700 underline text-sm font-medium" "Live routing analyzer"))
(p :class "text-green-900 font-medium" "What it enables")
(p :class "text-green-800" "After initial page load, pure pages render instantly without server roundtrips. Client matches routes locally, evaluates content expressions with cached components, and only falls back to server for pages with :data dependencies."))
(~doc-subsection :title "Architecture"
(p "Three-layer approach: spec defines pure route matching, page registry bridges server metadata to client, orchestration intercepts navigation for try-first/fallback.")
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Route matching spec (router.sx)")
(p "New spec module with pure functions for Flask-style route pattern matching:")
(~doc-code :code (highlight "(define split-path-segments ;; \"/docs/hello\" → (\"docs\" \"hello\")\n(define parse-route-pattern ;; \"/docs/<slug>\" → segment descriptors\n(define match-route-segments ;; segments + pattern → params dict or nil\n(define find-matching-route ;; path + route table → first match" "lisp"))
(p "No platform interface needed — uses only pure string and list primitives. Bootstrapped to both hosts via " (code "--spec-modules deps,router") "."))
(div
(h4 :class "font-semibold text-stone-700" "2. Page registry")
(p "Server serializes defpage metadata as SX dict literals inside " (code "<script type=\"text/sx-pages\">") ". Each entry carries name, path pattern, auth level, has-data flag, serialized content expression, and closure values.")
(~doc-code :code (highlight "{:name \"docs-page\" :path \"/docs/<slug>\"\n :auth \"public\" :has-data false\n :content \"(case slug ...)\" :closure {}}" "lisp"))
(p "boot.sx processes these at startup using the SX parser — the same " (code "parse") " function from parser.sx — building route entries with parsed patterns into the " (code "_page-routes") " table. No JSON dependency."))
(div
(h4 :class "font-semibold text-stone-700" "3. Client-side interception (orchestration.sx)")
(p (code "bind-client-route-link") " replaces " (code "bind-boost-link") " in boost processing. On click:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Extract pathname from href")
(li "Call " (code "find-matching-route") " against " (code "_page-routes"))
(li "If match found AND no :data: evaluate content expression locally with component env + URL params")
(li "If evaluation succeeds: swap into #main-panel, pushState, log " (code "\"sx:route client /path\""))
(li "If anything fails (no match, has data, eval error): transparent fallback to server fetch"))
(p (code "handle-popstate") " also tries client routing before server fetch on back/forward."))))
(~doc-subsection :title "What becomes client-routable"
(p "All pages with content expressions — most of this docs app. Pure pages render instantly; :data pages fetch data then render client-side (Phase 4):")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "/") ", " (code "/docs/") ", " (code "/docs/<slug>") " (most slugs), " (code "/protocols/") ", " (code "/protocols/<slug>"))
(li (code "/examples/") ", " (code "/examples/<slug>") ", " (code "/essays/") ", " (code "/essays/<slug>"))
(li (code "/plans/") ", " (code "/plans/<slug>") ", " (code "/isomorphism/") ", " (code "/bootstrappers/")))
(p "Pages that fall through to server:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "/docs/primitives") " and " (code "/docs/special-forms") " (call " (code "primitives-data") " / " (code "special-forms-data") " helpers)")
(li (code "/reference/<slug>") " (has " (code ":data (reference-data slug)") ")")
(li (code "/bootstrappers/<slug>") " (has " (code ":data (bootstrapper-data slug)") ")")
(li (code "/isomorphism/bundle-analyzer") " (has " (code ":data (bundle-analyzer-data)") ")")
(li (code "/isomorphism/data-test") " (has " (code ":data (data-test-data)") " — " (a :href "/isomorphism/data-test" :class "text-violet-700 underline" "Phase 4 demo") ")")))
(~doc-subsection :title "Try-first/fallback design"
(p "Client routing uses a try-first approach: attempt local evaluation in a try/catch, fall back to server fetch on any failure. This avoids needing perfect static analysis of content expressions — if a content expression calls a page helper the client doesn't have, the eval throws, and the server handles it transparently.")
(p "Console messages provide visibility: " (code "sx:route client /essays/why-sexps") " vs " (code "sx:route server /specs/eval") "."))
(~doc-subsection :title "Files"
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "shared/sx/ref/router.sx — route pattern matching spec")
(li "shared/sx/ref/boot.sx — process page registry scripts")
(li "shared/sx/ref/orchestration.sx — client route interception")
(li "shared/sx/ref/bootstrap_js.py — router spec module + platform functions")
(li "shared/sx/ref/bootstrap_py.py — router spec module (parity)")
(li "shared/sx/helpers.py — page registry SX serialization")))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Pure page navigation: zero server requests, console shows \"sx:route client\"")
(li "IO/data page fallback: falls through to server fetch transparently")
(li "Browser back/forward works with client-routed pages")
(li "Disabling page registry → identical behavior to before")
(li "Bootstrap parity: sx_ref.py and sx-ref.js both contain router functions"))))
;; -----------------------------------------------------------------------
;; Phase 4
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 4: Client Async & IO Bridge" :id "phase-4"
(div :class "rounded border border-green-300 bg-green-50 p-4 mb-4"
(div :class "flex items-center gap-2 mb-2"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
(a :href "/isomorphism/data-test" :class "text-green-700 underline text-sm font-medium" "Live data test page"))
(p :class "text-green-900 font-medium" "What it enables")
(p :class "text-green-800" "Client fetches server-evaluated data and renders :data pages locally. Data cached with TTL to avoid redundant fetches on back/forward navigation. All IO stays server-side — no continuations needed."))
(~doc-subsection :title "Architecture"
(p "Separates IO from rendering. Server evaluates :data expression (async, with DB/service access), serializes result as SX wire format. Client fetches pre-evaluated data, parses it, merges into env, renders pure :content client-side.")
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Abstract resolve-page-data")
(p "Spec-level primitive in orchestration.sx. The spec says \"I need data for this page\" — platform provides transport:")
(~doc-code :code (highlight "(resolve-page-data page-name params\n (fn (data)\n ;; data is a dict — merge into env and render\n (let ((env (merge closure params data))\n (rendered (try-eval-content content-src env)))\n (swap-rendered-content target rendered pathname))))" "lisp"))
(p "Browser platform: HTTP fetch to " (code "/sx/data/<page-name>") ". Future platforms could use IPC, cache, WebSocket, etc."))
(div
(h4 :class "font-semibold text-stone-700" "2. Server data endpoint")
(p (code "evaluate_page_data()") " evaluates the :data expression, kebab-cases dict keys (Python " (code "total_count") " → SX " (code "total-count") "), serializes as SX wire format.")
(p "Response content type: " (code "text/sx; charset=utf-8") ". Per-page auth enforcement via " (code "_check_page_auth()") "."))
(div
(h4 :class "font-semibold text-stone-700" "3. Client data cache")
(p "In-memory cache in orchestration.sx, keyed by " (code "page-name:param=value") ". 30-second TTL prevents redundant fetches on back/forward navigation:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Cache miss: " (code "sx:route client+data /path") " — fetches from server, caches, renders")
(li "Cache hit: " (code "sx:route client+cache /path") " — instant render from cached data")
(li "After TTL: stale entry evicted, fresh fetch on next visit"))
(p "Try it: navigate to the " (a :href "/isomorphism/data-test" :class "text-violet-700 underline" "data test page") ", go back, return within 30s — the server-time stays the same (cached). Wait 30s+ and return — new time (fresh fetch)."))))
(~doc-subsection :title "Files"
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "shared/sx/ref/orchestration.sx — resolve-page-data spec, data cache")
(li "shared/sx/ref/bootstrap_js.py — platform resolvePageData (HTTP fetch)")
(li "shared/sx/pages.py — evaluate_page_data(), auto_mount_page_data()")
(li "shared/sx/helpers.py — deps for :data pages in page registry")
(li "sx/sx/data-test.sx — test component")
(li "shared/sx/tests/test_page_data.py — 30 unit tests")))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "30 unit tests: serialize roundtrip, kebab-case, deps, full pipeline simulation, cache TTL")
(li "Console: " (code "sx:route client+data") " on first visit, " (code "sx:route client+cache") " on return within 30s")
(li (a :href "/isomorphism/data-test" :class "text-violet-700 underline" "Live data test page") " exercises the full pipeline with server time + pipeline steps")
(li "append! and dict-set! registered as proper primitives in spec + both hosts"))))
;; -----------------------------------------------------------------------
;; Phase 5
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 5: Client IO Proxy" :id "phase-5"
(div :class "rounded border border-green-300 bg-green-50 p-4 mb-4"
(div :class "flex items-center gap-2 mb-2"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
(p :class "text-green-900 font-medium" "What it enables")
(p :class "text-green-800" "Components with IO dependencies render client-side. IO primitives are proxied to the server — the client evaluator calls them like normal functions, the proxy fetches results via HTTP, the async DOM renderer awaits the promises and continues."))
(~doc-subsection :title "How it works"
(p "Instead of async-aware continuations (originally planned), Phase 5 was solved by combining three mechanisms that emerged from Phases 3-4:")
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. IO dependency detection (from Phase 2)")
(p "The component dep analyzer scans AST bodies for IO primitive names (highlight, asset-url, query, frag, etc.) and computes transitive IO refs. Pages include their IO dep list in the page registry."))
(div
(h4 :class "font-semibold text-stone-700" "2. IO proxy registration")
(p (code "registerIoDeps(names)") " in orchestration.sx registers proxy functions for each IO primitive. When the client evaluator encounters " (code "(highlight code \"sx\")") ", the proxy sends an HTTP request to the server's IO endpoint and returns a Promise."))
(div
(h4 :class "font-semibold text-stone-700" "3. Async DOM renderer")
(p (code "asyncRenderToDom") " walks the expression tree and handles Promises transparently. When a subexpression returns a Promise (from an IO proxy call), the renderer awaits it and continues building the DOM tree. No continuations needed — JavaScript's native Promise mechanism provides the suspension."))))
(~doc-subsection :title "Why continuations weren't needed"
(p "The original Phase 5 plan called for async-aware shift/reset or a CPS transform of the evaluator. In practice, JavaScript's Promise mechanism provided the same capability: the async DOM renderer naturally suspends when it encounters a Promise and resumes when it resolves.")
(p "Delimited continuations remain valuable for Phase 6 (streaming/suspense on the " (em "server") " side, where Python doesn't have native Promise-based suspension in the evaluator). But for client-side IO, Promises + async render were sufficient."))
(~doc-subsection :title "Files"
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "shared/sx/ref/orchestration.sx — registerIoDeps, IO proxy registration")
(li "shared/sx/ref/bootstrap_js.py — asyncRenderToDom, IO proxy HTTP transport")
(li "shared/sx/helpers.py — io_deps in page registry entries")
(li "shared/sx/deps.py — transitive IO ref computation")))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Navigate to any page with IO deps (e.g. /testing/eval) — console shows IO proxy calls")
(li "Components using " (code "highlight") " render correctly via proxy")
(li "Pages with " (code "asset-url") " resolve script paths via proxy")
(li "Async render completes without blocking — partial results appear as promises resolve"))))
;; -----------------------------------------------------------------------
;; Phase 6
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 6: Streaming & Suspense" :id "phase-6"
(div :class "rounded border border-green-300 bg-green-50 p-4 mb-4"
(div :class "flex items-center gap-2 mb-2"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
(a :href "/isomorphism/streaming" :class "text-green-700 underline text-sm font-medium" "Live streaming demo"))
(p :class "text-green-900 font-medium" "What it enables")
(p :class "text-green-800" "Server streams partially-evaluated SX as IO resolves. Client renders available subtrees immediately with loading skeletons, fills in suspended parts as data arrives."))
(~doc-subsection :title "What was built"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "~suspense") " component — renders fallback content with a stable DOM ID, replaced when resolution arrives")
(li (code "defpage :stream true") " — opts a page into streaming response mode")
(li (code "defpage :fallback expr") " — custom loading skeleton for streaming pages")
(li (code "execute_page_streaming()") " — Quart async generator response that yields HTML chunks")
(li (code "sx_page_streaming_parts()") " — splits the HTML shell into streamable parts")
(li (code "Sx.resolveSuspense(id, sx)") " — client-side function to replace suspense placeholders")
(li (code "window.__sxResolve") " bootstrap — queues resolutions that arrive before sx.js loads")
(li "Concurrent IO: data eval + header eval run in parallel via " (code "asyncio.create_task"))
(li "Completion-order streaming: whichever IO finishes first gets sent first via " (code "asyncio.wait(FIRST_COMPLETED)"))))
(~doc-subsection :title "Architecture"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Suspense component")
(p "When streaming, the server renders the page with " (code "~suspense") " placeholders instead of awaiting IO:")
(~doc-code :code (highlight "(~app-body\n :header-rows (~suspense :id \"stream-headers\"\n :fallback (div :class \"h-12 bg-stone-200 animate-pulse\"))\n :content (~suspense :id \"stream-content\"\n :fallback (div :class \"p-8 animate-pulse\" ...)))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "2. Chunked transfer")
(p "Quart async generator response yields chunks in order:")
(ol :class "list-decimal pl-5 text-stone-700 space-y-1"
(li "HTML shell + CSS + component defs + page registry + suspense page SX + scripts (immediate)")
(li "Resolution " (code "<script>") " tags as each IO completes")
(li "Closing " (code "</body></html>"))))
(div
(h4 :class "font-semibold text-stone-700" "3. Client resolution")
(p "Each resolution chunk is an inline script:")
(~doc-code :code (highlight "<script>\n window.__sxResolve(\"stream-content\",\n \"(~article :title \\\"Hello\\\")\")\n</script>" "html"))
(p "The client parses the SX, renders to DOM, and replaces the suspense placeholder's children."))
(div
(h4 :class "font-semibold text-stone-700" "4. Concurrent IO")
(p "Data evaluation and header construction run in parallel. " (code "asyncio.wait(FIRST_COMPLETED)") " yields resolution chunks in whatever order IO completes — no artificial sequencing."))))
(~doc-subsection :title "Continuation foundation"
(p "Delimited continuations (" (code "reset") "/" (code "shift") ") are implemented in the Python evaluator (async_eval.py lines 586-624) and available as special forms. Phase 6 uses the simpler pattern of concurrent IO + completion-order streaming, but the continuation machinery is in place for Phase 7's more sophisticated evaluation-level suspension."))
(~doc-subsection :title "Files"
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "shared/sx/templates/pages.sx — ~suspense component definition")
(li "shared/sx/types.py — PageDef.stream, PageDef.fallback_expr fields")
(li "shared/sx/evaluator.py — defpage :stream/:fallback parsing")
(li "shared/sx/pages.py — execute_page_streaming(), streaming route mounting")
(li "shared/sx/helpers.py — sx_page_streaming_parts(), sx_streaming_resolve_script()")
(li "shared/sx/ref/boot.sx — resolve-suspense spec (canonical)")
(li "shared/sx/ref/bootstrap_js.py — resolveSuspense on Sx object, __sxPending/Resolve init")
(li "shared/static/scripts/sx-browser.js — bootstrapped output (DO NOT EDIT)")
(li "shared/sx/async_eval.py — reset/shift special forms (continuation foundation)")
(li "sx/sx/streaming-demo.sx — demo content component")
(li "sx/sxc/pages/docs.sx — streaming-demo defpage")
(li "sx/sxc/pages/helpers.py — streaming-demo-data page helper")))
(~doc-subsection :title "Demonstration"
(p "The " (a :href "/isomorphism/streaming" :class "text-violet-700 underline" "streaming demo page") " exercises the full pipeline:")
(ol :class "list-decimal pl-5 text-stone-700 space-y-1"
(li "Navigate to " (a :href "/isomorphism/streaming" :class "text-violet-700 underline" "/isomorphism/streaming"))
(li "The page skeleton appears " (strong "instantly") " — animated loading skeletons fill the content area")
(li "After ~1.5 seconds, the real content replaces the skeletons (streamed from server)")
(li "Open the Network tab — observe " (code "Transfer-Encoding: chunked") " on the document response")
(li "The document response shows multiple chunks arriving over time: shell first, then resolution scripts")))
(~doc-subsection :title "What to verify"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Instant shell: ") "The page HTML arrives immediately — no waiting for the 1.5s data fetch")
(li (strong "Suspense placeholders: ") "The " (code "~suspense") " component renders a " (code "data-suspense") " wrapper with animated fallback content")
(li (strong "Resolution: ") "The " (code "__sxResolve()") " inline script replaces the placeholder with real rendered content")
(li (strong "Chunked encoding: ") "Network tab shows the document as a chunked response with multiple frames")
(li (strong "Concurrent IO: ") "Header and content resolve independently — whichever finishes first appears first")
(li (strong "HTMX fallback: ") "SX/HTMX requests bypass streaming and receive a standard response"))))
;; -----------------------------------------------------------------------
;; Phase 7
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 7: Full Isomorphism" :id "phase-7"
(div :class "rounded border border-green-200 bg-green-50 p-4 mb-4"
(div :class "flex items-center gap-2 mb-2"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
(p :class "text-green-900 font-medium" "What it enables")
(p :class "text-green-800" "Same SX code runs on either side. Runtime chooses optimal split via affinity annotations and render plans. Client data cache managed via invalidation headers and server-driven updates. Cross-host isomorphism verified by 61 automated tests."))
(~doc-subsection :title "7a. Affinity Annotations & Render Target"
(div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
(p :class "text-green-800 text-sm" "Components declare where they prefer to render. The spec combines affinity with IO analysis to produce a per-component render target decision."))
(p "Affinity annotations let component authors express rendering preferences:")
(~doc-code :code (highlight "(defcomp ~product-grid (&key products)\n :affinity :client ;; interactive, prefer client rendering\n (div ...))\n\n(defcomp ~auth-menu (&key user)\n :affinity :server ;; auth-sensitive, always server\n (div ...))\n\n(defcomp ~card (&key title)\n ;; no annotation = :affinity :auto (default)\n ;; runtime decides from IO analysis\n (div ...))" "lisp"))
(p "The " (code "render-target") " function in deps.sx combines affinity with IO analysis:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code ":affinity :server") " → always " (code "\"server\"") " (auth-sensitive, secrets, heavy IO)")
(li (code ":affinity :client") " → always " (code "\"client\"") " (interactive, IO proxied)")
(li (code ":affinity :auto") " (default) → " (code "\"server\"") " if IO-dependent, " (code "\"client\"") " if pure"))
(p "The server's partial evaluator (" (code "_aser") ") uses " (code "render_target") " instead of the previous " (code "is_pure") " check. Components with " (code ":affinity :client") " are serialized for client rendering even if they call IO primitives — the IO proxy (Phase 5) handles the calls.")
(~doc-subsection :title "Files"
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "shared/sx/ref/eval.sx — defcomp annotation parsing, defcomp-kwarg helper")
(li "shared/sx/ref/deps.sx — render-target function, platform interface")
(li "shared/sx/types.py — Component.affinity field, render_target property")
(li "shared/sx/evaluator.py — _sf_defcomp annotation extraction")
(li "shared/sx/async_eval.py — _aser uses render_target")
(li "shared/sx/ref/bootstrap_js.py — Component.affinity, componentAffinity()")
(li "shared/sx/ref/bootstrap_py.py — component_affinity(), make_component()")
(li "shared/sx/ref/test-eval.sx — 4 new defcomp affinity tests")
(li "shared/sx/ref/test-deps.sx — 6 new render-target tests")))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "269 spec tests pass (10 new: 4 eval + 6 deps)")
(li "79 Python unit tests pass")
(li "Bootstrapped to both hosts (sx_ref.py + sx-browser.js)")
(li "Backward compatible: existing defcomp without :affinity defaults to \"auto\""))))
(~doc-subsection :title "7b. Runtime Boundary Optimizer"
(div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
(p :class "text-green-800 text-sm" "Per-page render plans computed at registration time. Each page knows exactly which components render server-side vs client-side, cached on PageDef."))
(p "Given component tree + IO dependency graph + affinity annotations, decide per-component: server-expand, client-render, or stream. Planning step cached at registration, recomputed on component change.")
(p (code "page-render-plan") " in deps.sx computes per-page boundary decisions:")
(~doc-code :code (highlight "(page-render-plan page-source env io-names)\n;; Returns:\n;; {:components {~name \"server\"|\"client\" ...}\n;; :server (list of server-expanded names)\n;; :client (list of client-rendered names)\n;; :io-deps (IO primitives needed by server components)}" "lisp"))
(~doc-subsection :title "Integration Points"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "shared/sx/ref/deps.sx") " — " (code "page-render-plan") " spec function")
(li (code "shared/sx/deps.py") " — Python wrapper, dispatches to bootstrapped code")
(li (code "shared/sx/pages.py") " — " (code "compute_page_render_plans()") " called at mount time, caches on PageDef")
(li (code "shared/sx/helpers.py") " — " (code "_build_pages_sx()") " includes " (code ":render-plan") " in client page registry")
(li (code "shared/sx/types.py") " — " (code "PageDef.render_plan") " field")))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "5 new spec tests (page-render-plan suite)")
(li "Render plans visible on " (a :href "/isomorphism/affinity" "affinity demo page"))
(li "Client page registry includes :render-plan for each page"))))
(~doc-subsection :title "7c. Cache Invalidation & Optimistic Data Updates"
(div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
(p :class "text-green-800 text-sm" "Client data cache management, optimistic predicted mutations with snapshot rollback, and server-driven cache updates."))
(p "The client-side page data cache (30-second TTL) now supports cache invalidation, server-driven updates, and optimistic mutations. The client predicts the result of a mutation, immediately re-renders with the predicted data, and confirms or reverts when the server responds.")
(~doc-subsection :title "Cache Invalidation"
(p "Component authors can declare cache invalidation on elements that trigger mutations:")
(~doc-code :code (highlight ";; Clear specific page's cache after successful action\n(form :sx-post \"/cart/remove\"\n :sx-cache-invalidate \"cart-page\"\n ...)\n\n;; Clear ALL page caches after action\n(button :sx-post \"/admin/reset\"\n :sx-cache-invalidate \"*\")" "lisp"))
(p "The server can also control client cache via response headers:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "SX-Cache-Invalidate: page-name") " — clear cache for a page")
(li (code "SX-Cache-Update: page-name") " — replace cache with the response body (SX-format data)")))
(~doc-subsection :title "Optimistic Mutations"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "optimistic-cache-update") " — applies a mutator function to cached data, saves a snapshot for rollback")
(li (strong "optimistic-cache-revert") " — restores the pre-mutation snapshot if the server rejects")
(li (strong "optimistic-cache-confirm") " — discards the snapshot after server confirmation")
(li (strong "submit-mutation") " — orchestration function: predict, submit, confirm/revert")
(li (strong "/sx/action/<name>") " — server endpoint for processing mutations (POST, returns SX wire format)")))
(~doc-subsection :title "Files"
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "shared/sx/ref/orchestration.sx — cache management + optimistic cache functions + submit-mutation spec")
(li "shared/sx/ref/engine.sx — SX-Cache-Invalidate, SX-Cache-Update response headers")
(li "shared/sx/pages.py — mount_action_endpoint for /sx/action/<name>")
(li "sx/sx/optimistic-demo.sx — live demo component")))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Live demo at " (a :href "/isomorphism/optimistic" :class "text-violet-600 hover:underline" "/isomorphism/optimistic"))
(li "Console log: " (code "sx:optimistic confirmed") " / " (code "sx:optimistic reverted")))))
(~doc-subsection :title "7d. Offline Data Layer"
(div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
(p :class "text-green-800 text-sm" "Service Worker with IndexedDB caching, connectivity tracking, and offline mutation queue with replay on reconnect."))
(p "A Service Worker registered at " (code "/sx-sw.js") " provides three-tier caching, plus an offline mutation queue that builds on Phase 7c's optimistic updates:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "/sx/data/* ") "— network-first with IndexedDB fallback. Page data cached on fetch, served from IndexedDB when offline.")
(li (strong "/sx/io/* ") "— network-first with IndexedDB fallback. IO proxy responses cached the same way.")
(li (strong "/static/* ") "— stale-while-revalidate via Cache API. Serves cached assets immediately, updates in background.")
(li (strong "Offline mutations") " — " (code "offline-aware-mutation") " routes to " (code "submit-mutation") " when online, " (code "offline-queue-mutation") " when offline. " (code "offline-sync") " replays the queue on reconnect."))
(~doc-subsection :title "How It Works"
(ol :class "list-decimal list-inside text-stone-700 space-y-2"
(li "On boot, " (code "sx-browser.js") " registers the SW at " (code "/sx-sw.js") " (root scope)")
(li "SW intercepts fetch events and routes by URL pattern")
(li "For data/IO: try network first, on failure serve from IndexedDB")
(li "For static assets: serve from Cache API, revalidate in background")
(li "Cache invalidation propagates: element attr / response header → in-memory cache → SW message → IndexedDB")
(li "Offline mutations queue locally, replay on reconnect via " (code "offline-sync"))))
(~doc-subsection :title "Files"
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "shared/static/scripts/sx-sw.js — Service Worker (network-first + stale-while-revalidate)")
(li "shared/sx/ref/orchestration.sx — offline queue, sync, connectivity tracking, sw-post-message")
(li "shared/sx/pages.py — mount_service_worker() serves SW at /sx-sw.js")
(li "sx/sx/offline-demo.sx — live demo component")))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Live demo at " (a :href "/isomorphism/offline" :class "text-violet-600 hover:underline" "/isomorphism/offline"))
(li "Test with DevTools Network → Offline mode")
(li "Console log: " (code "sx:offline queued") ", " (code "sx:offline syncing") ", " (code "sx:offline synced")))))
(~doc-subsection :title "7e. Isomorphic Testing"
(div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
(p :class "text-green-800 text-sm" "Cross-host test suite: same SX expressions evaluated on Python (sx_ref.py) and JS (sx-browser.js via Node.js), HTML output compared."))
(p "61 isomorphic tests verify that Python and JS produce identical results:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "37 eval tests: arithmetic, comparison, strings, collections, logic, let/lambda, higher-order, dict, keywords, cond/case")
(li "24 render tests: elements, attributes, nesting, void elements, boolean attrs, conditionals, map, components, affinity, HTML escaping"))
(~doc-subsection :title "Files"
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "shared/sx/tests/test_isomorphic.py — cross-host test suite")
(li "Run: " (code "python3 -m pytest shared/sx/tests/test_isomorphic.py -q")))))
(~doc-subsection :title "7f. Universal Page Descriptor"
(div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
(p :class "text-green-800 text-sm" "defpage is portable: same descriptor executes on server (execute_page) and client (tryClientRoute)."))
(p "The defpage descriptor is universal — the same definition works on both hosts:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Server: ") (code "execute_page()") " evaluates :data and :content slots, expands server components via " (code "_aser") ", returns SX wire format")
(li (strong "Client: ") (code "try-client-route") " matches route, evaluates content SX, renders to DOM. Data pages fetch via " (code "/sx/data/") ", IO proxied via " (code "/sx/io/"))
(li (strong "Render plan: ") "each page's " (code ":render-plan") " is included in the client page registry, showing which components render where")
(li (strong "Console visibility: ") "client logs " (code "sx:route plan pagename — N server, M client") " on each navigation")))
(div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2"
(p :class "text-amber-800 text-sm" (strong "Depends on: ") "All previous phases.")))
;; -----------------------------------------------------------------------
;; Cross-Cutting Concerns
;; -----------------------------------------------------------------------
(~doc-section :title "Cross-Cutting Concerns" :id "cross-cutting"
(~doc-subsection :title "Error Reporting"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Phase 1: \"Unknown component\" includes which page expected it and what bundle was sent")
(li "Phase 2: Server logs which components expanded server-side vs sent to client")
(li "Phase 3: Client route failures include unmatched path and available routes")
(li "Phase 4: Client data errors include page name, params, server response status")
(li "Source location tracking in parser → propagate through eval → include in error messages")))
(~doc-subsection :title "Backward Compatibility"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Pages without annotations behave as today")
(li "SX-Request / SX-Components / SX-Css header protocol continues")
(li "Existing .sx files require no changes")
(li "_expand_components continues as override")
(li "Each phase is opt-in: disable → identical to previous behavior")))
(~doc-subsection :title "Spec Integrity"
(p "All new behavior specified in .sx files under shared/sx/ref/ before implementation. Bootstrappers transpile from spec. This ensures JS and Python stay in sync.")))
;; -----------------------------------------------------------------------
;; Critical Files
;; -----------------------------------------------------------------------
(~doc-section :title "Critical Files" :id "critical-files"
(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")
(th :class "px-3 py-2 font-medium text-stone-600" "Phases")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/async_eval.py")
(td :class "px-3 py-2 text-stone-700" "Core evaluator, _aser, server/client boundary")
(td :class "px-3 py-2 text-stone-600" "2, 5"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/helpers.py")
(td :class "px-3 py-2 text-stone-700" "sx_page(), sx_response(), output pipeline")
(td :class "px-3 py-2 text-stone-600" "1, 3"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/jinja_bridge.py")
(td :class "px-3 py-2 text-stone-700" "_COMPONENT_ENV, component registry")
(td :class "px-3 py-2 text-stone-600" "1, 2"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/pages.py")
(td :class "px-3 py-2 text-stone-700" "defpage, execute_page(), page lifecycle")
(td :class "px-3 py-2 text-stone-600" "2, 3"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/boot.sx")
(td :class "px-3 py-2 text-stone-700" "Client boot, component caching")
(td :class "px-3 py-2 text-stone-600" "1, 3, 4"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/orchestration.sx")
(td :class "px-3 py-2 text-stone-700" "Client fetch/swap/morph")
(td :class "px-3 py-2 text-stone-600" "3, 4"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/eval.sx")
(td :class "px-3 py-2 text-stone-700" "Evaluator spec")
(td :class "px-3 py-2 text-stone-600" "4"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/engine.sx")
(td :class "px-3 py-2 text-stone-700" "Morph, swaps, triggers")
(td :class "px-3 py-2 text-stone-600" "3"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/deps.py")
(td :class "px-3 py-2 text-stone-700" "Dependency analysis (new)")
(td :class "px-3 py-2 text-stone-600" "1, 2"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/router.sx")
(td :class "px-3 py-2 text-stone-700" "Client-side routing (new)")
(td :class "px-3 py-2 text-stone-600" "3"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/io-bridge.sx")
(td :class "px-3 py-2 text-stone-700" "Client IO primitives (new)")
(td :class "px-3 py-2 text-stone-600" "4"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/suspense.sx")
(td :class "px-3 py-2 text-stone-700" "Streaming/suspension (new)")
(td :class "px-3 py-2 text-stone-600" "5"))))))))
;; ---------------------------------------------------------------------------
;; SX CI Pipeline
;; ---------------------------------------------------------------------------

View File

@@ -0,0 +1,110 @@
;; ---------------------------------------------------------------------------
;; Live Streaming — SSE & WebSocket
;; ---------------------------------------------------------------------------
(defcomp ~plan-live-streaming-content ()
(~doc-page :title "Live Streaming"
(~doc-section :title "Context" :id "context"
(p "SX streaming currently uses chunked transfer encoding: the server sends an HTML shell with "
(code "~suspense") " placeholders, then resolves each one via inline "
(code "<script>__sxResolve(id, sx)</script>") " chunks as IO completes. "
"Once the response finishes, the connection closes. Each slot resolves exactly once.")
(p "This is powerful for initial page load but doesn't support live updates "
"— dashboard metrics, chat messages, collaborative editing, real-time notifications. "
"For that we need a persistent transport: " (strong "SSE") " (Server-Sent Events) or " (strong "WebSockets") ".")
(p "The key insight: the client already has " (code "Sx.resolveSuspense(id, sxSource)") " which replaces "
"DOM content by suspense ID. A persistent connection just needs to keep calling it."))
(~doc-section :title "Design" :id "design"
(~doc-subsection :title "Transport Hierarchy"
(p "Three tiers, progressively more capable:")
(ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
(li (strong "Chunked streaming") " (done) — single HTTP response, each suspense resolves once. "
"Best for: initial page load with slow IO.")
(li (strong "SSE") " — persistent one-way connection, server pushes resolve events. "
"Best for: dashboards, notifications, progress bars, any read-only live data.")
(li (strong "WebSocket") " — bidirectional, client can send events back. "
"Best for: chat, collaborative editing, interactive applications.")))
(~doc-subsection :title "SSE Protocol"
(p "A " (code "~live") " component declares a persistent connection to an SSE endpoint:")
(~doc-code :code (highlight "(~live :src \"/api/stream/dashboard\"\n (~suspense :id \"cpu\" :fallback (span \"Loading...\"))\n (~suspense :id \"memory\" :fallback (span \"Loading...\"))\n (~suspense :id \"requests\" :fallback (span \"Loading...\")))" "lisp"))
(p "The server SSE endpoint yields SX resolve events:")
(~doc-code :code (highlight "async def dashboard_stream():\n while True:\n stats = await get_system_stats()\n yield sx_sse_event(\"cpu\", f'(~stat-badge :value \"{stats.cpu}%\")')\n yield sx_sse_event(\"memory\", f'(~stat-badge :value \"{stats.mem}%\")')\n await asyncio.sleep(1)" "python"))
(p "SSE wire format — each event is a suspense resolve:")
(~doc-code :code (highlight "event: sx-resolve\ndata: {\"id\": \"cpu\", \"sx\": \"(~stat-badge :value \\\"42%\\\")\"}\n\nevent: sx-resolve\ndata: {\"id\": \"memory\", \"sx\": \"(~stat-badge :value \\\"68%\\\")\"}" "text")))
(~doc-subsection :title "WebSocket Protocol"
(p "A " (code "~ws") " component establishes a bidirectional channel:")
(~doc-code :code (highlight "(~ws :src \"/ws/chat\"\n :on-message handle-chat-message\n (~suspense :id \"messages\" :fallback (div \"Connecting...\"))\n (~suspense :id \"typing\" :fallback (span)))" "lisp"))
(p "Client can send SX expressions back:")
(~doc-code :code (highlight ";; Client sends:\n(sx-send ws-conn '(chat-message :text \"hello\" :user \"alice\"))\n\n;; Server receives, broadcasts to all connected clients:\n;; event: sx-resolve for \"messages\" suspense" "lisp")))
(~doc-subsection :title "Shared Resolution Mechanism"
(p "All three transports use the same client-side resolution:")
(ul :class "list-disc list-inside space-y-1 text-stone-600 text-sm"
(li (code "Sx.resolveSuspense(id, sxSource)") " — already exists, parses SX and renders to DOM")
(li "SSE: " (code "EventSource") " → " (code "onmessage") " → " (code "resolveSuspense()"))
(li "WS: " (code "WebSocket") " → " (code "onmessage") " → " (code "resolveSuspense()"))
(li "The component env (defs needed for rendering) can be sent once on connection open")
(li "Subsequent events only need the SX expression — lightweight wire format"))))
(~doc-section :title "Implementation" :id "implementation"
(~doc-subsection :title "Phase 1: SSE Infrastructure"
(ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
(li "Add " (code "~live") " component to " (code "shared/sx/templates/") " — renders child suspense placeholders, "
"emits " (code "data-sx-live") " attribute with SSE endpoint URL")
(li "Add " (code "sx-live.js") " client module — on boot, finds " (code "[data-sx-live]") " elements, "
"opens EventSource, routes events to " (code "resolveSuspense()"))
(li "Add " (code "sx_sse_event(id, sx)") " helper for Python SSE endpoints — formats SSE wire protocol")
(li "Add " (code "sse_stream()") " Quart helper — returns async generator Response with correct headers")))
(~doc-subsection :title "Phase 2: Defpage Integration"
(ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
(li "New " (code ":live") " defpage slot — declares SSE endpoint + suspense bindings")
(li "Auto-mount SSE endpoint alongside the page route")
(li "Component defs sent as first SSE event on connection open")
(li "Automatic reconnection with exponential backoff")))
(~doc-subsection :title "Phase 3: WebSocket"
(ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
(li "Add " (code "~ws") " component — bidirectional channel with send/receive")
(li "Add " (code "sx-ws.js") " client module — WebSocket management, message routing")
(li "Server-side: Quart WebSocket handlers that receive and broadcast SX events")
(li "Client-side: " (code "sx-send") " primitive for sending SX expressions to server")))
(~doc-subsection :title "Phase 4: Spec & Boundary"
(ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
(li "Spec " (code "~live") " and " (code "~ws") " in " (code "render.sx") " (how they render in each mode)")
(li "Add SSE/WS IO primitives to " (code "boundary.sx"))
(li "Bootstrap SSE/WS connection management into " (code "sx-ref.js"))
(li "Spec-level tests for resolve, reconnection, and message routing"))))
(~doc-section :title "Files" :id "files"
(table :class "w-full text-left border-collapse"
(thead
(tr :class "border-b border-stone-200"
(th :class "px-3 py-2 font-medium text-stone-600" "File")
(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 font-mono text-sm text-violet-700" "shared/sx/templates/live.sx")
(td :class "px-3 py-2 text-stone-700" "~live component definition"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/static/scripts/sx-live.js")
(td :class "px-3 py-2 text-stone-700" "SSE client — EventSource → resolveSuspense"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/sse.py")
(td :class "px-3 py-2 text-stone-700" "SSE helpers — event formatting, stream response"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/static/scripts/sx-ws.js")
(td :class "px-3 py-2 text-stone-700" "WebSocket client — bidirectional SX channel"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/render.sx")
(td :class "px-3 py-2 text-stone-700" "Spec: ~live and ~ws rendering in all modes"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/boundary.sx")
(td :class "px-3 py-2 text-stone-700" "SSE/WS IO primitive declarations")))))))

View File

@@ -0,0 +1,257 @@
;; ---------------------------------------------------------------------------
;; Predictive Component Prefetching
;; ---------------------------------------------------------------------------
(defcomp ~plan-predictive-prefetch-content ()
(~doc-page :title "Predictive Component Prefetching"
(~doc-section :title "Context" :id "context"
(p "Phase 3 of the isomorphic roadmap added client-side routing with component dependency checking. When a user clicks a link, " (code "try-client-route") " checks " (code "has-all-deps?") " — if the target page needs components not yet loaded, the client falls back to a server fetch. This works correctly but misses an opportunity: " (strong "we can prefetch those missing components before the click happens."))
(p "The page registry already carries " (code ":deps") " metadata for every page. The client already knows which components are loaded via " (code "loaded-component-names") ". The gap is a mechanism to " (em "proactively") " resolve the difference — fetching missing component definitions so that by the time the user clicks, client-side routing succeeds.")
(p "But this goes beyond just hover-to-prefetch. The full spectrum includes: bundling linked routes' components with the initial page load, batch-prefetching after idle, predicting mouse trajectory toward links, and even splitting the component/data fetch so that " (code ":data") " pages can prefetch their components and only fetch data on click. Each strategy trades bandwidth for latency, and pages should be able to declare which tradeoff they want."))
(~doc-section :title "Current State" :id "current-state"
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "Layer")
(th :class "px-3 py-2 font-medium text-stone-600" "What exists")
(th :class "px-3 py-2 font-medium text-stone-600" "Where")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Page registry")
(td :class "px-3 py-2 text-stone-700" "Each page carries " (code ":deps (\"~card\" \"~essay-foo\" ...)"))
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "helpers.py → <script type=\"text/sx-pages\">"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Dep check")
(td :class "px-3 py-2 text-stone-700" (code "has-all-deps?") " gates client routing")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "orchestration.sx:546-559"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Component bundle")
(td :class "px-3 py-2 text-stone-700" "Per-page inline " (code "<script type=\"text/sx\" data-components>"))
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "helpers.py:715, jinja_bridge.py"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Incremental defs")
(td :class "px-3 py-2 text-stone-700" (code "components_for_request()") " sends only missing defs in SX responses")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "helpers.py:459-509"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Preload cache")
(td :class "px-3 py-2 text-stone-700" (code "sx-preload") " prefetches full responses on hover/mousedown")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "orchestration.sx:686-708"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Route matching")
(td :class "px-3 py-2 text-stone-700" (code "find-matching-route") " matches pathname to page entry")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "router.sx"))))))
;; -----------------------------------------------------------------------
;; Prefetch strategies
;; -----------------------------------------------------------------------
(~doc-section :title "Prefetch Strategies" :id "strategies"
(p "Prefetching is a spectrum from conservative to aggressive. The system should support all of these, configured declaratively per link or per page via " (code "defpage") " metadata and " (code "sx-prefetch") " attributes.")
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "Strategy")
(th :class "px-3 py-2 font-medium text-stone-600" "Trigger")
(th :class "px-3 py-2 font-medium text-stone-600" "What prefetches")
(th :class "px-3 py-2 font-medium text-stone-600" "Latency on click")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Eager bundle")
(td :class "px-3 py-2 text-stone-700" "Initial page load")
(td :class "px-3 py-2 text-stone-700" "Components for linked routes included in " (code "<script data-components>"))
(td :class "px-3 py-2 text-stone-600" "Zero — already in memory"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Idle timer")
(td :class "px-3 py-2 text-stone-700" "After page settles (requestIdleCallback or setTimeout)")
(td :class "px-3 py-2 text-stone-700" "Components for visible nav links, batched in one request")
(td :class "px-3 py-2 text-stone-600" "Zero if idle fetch completed"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Viewport")
(td :class "px-3 py-2 text-stone-700" "Link scrolls into view (IntersectionObserver)")
(td :class "px-3 py-2 text-stone-700" "Components for that link's route")
(td :class "px-3 py-2 text-stone-600" "Zero if user scrolled before clicking"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Mouse approach")
(td :class "px-3 py-2 text-stone-700" "Cursor moving toward link (trajectory prediction)")
(td :class "px-3 py-2 text-stone-700" "Components for predicted target")
(td :class "px-3 py-2 text-stone-600" "Near-zero — fetch starts ~200ms before hover"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Hover")
(td :class "px-3 py-2 text-stone-700" "mouseover (150ms debounce)")
(td :class "px-3 py-2 text-stone-700" "Components for hovered link's route")
(td :class "px-3 py-2 text-stone-600" "Low — typical hover-to-click is 300-500ms"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Mousedown")
(td :class "px-3 py-2 text-stone-700" "mousedown (0ms debounce)")
(td :class "px-3 py-2 text-stone-700" "Components for clicked link's route")
(td :class "px-3 py-2 text-stone-600" "~80ms — mousedown-to-click gap"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Components + data")
(td :class "px-3 py-2 text-stone-700" "Any of the above")
(td :class "px-3 py-2 text-stone-700" "Components " (em "and") " page data for " (code ":data") " pages")
(td :class "px-3 py-2 text-stone-600" "Zero for components; data fetch may still be in flight")))))
(~doc-subsection :title "Eager Bundle"
(p "The server already computes per-page component bundles. For key navigation paths — the main nav bar, section nav — the server can include " (em "linked routes' components") " in the initial bundle, not just the current page's.")
(~doc-code :code (highlight ";; defpage metadata declares eager prefetch targets\n(defpage docs-page\n :path \"/docs/<slug>\"\n :auth :public\n :prefetch :eager ;; bundle deps for all linked pure routes\n :content (case slug ...))" "lisp"))
(p "Implementation: " (code "components_for_page()") " already scans the page SX for component refs. Extend it to also scan for " (code "href") " attributes, match them against the page registry, and include those pages' deps in the bundle. The cost is a larger initial payload; the benefit is zero-latency navigation within a section."))
(~doc-subsection :title "Idle Timer"
(p "After page load and initial render, use " (code "requestIdleCallback") " (or a fallback " (code "setTimeout") ") to scan visible nav links and batch-prefetch their missing components in a single request.")
(~doc-code :code (highlight "(define prefetch-visible-links-on-idle\n (fn ()\n (request-idle-callback\n (fn ()\n (let ((links (dom-query-all \"a[href][sx-get]\"))\n (all-missing (list)))\n (for-each\n (fn (link)\n (let ((missing (compute-missing-deps\n (url-pathname (dom-get-attr link \"href\")))))\n (when missing\n (for-each (fn (d) (append! all-missing d))\n missing))))\n links)\n (when (not (empty? all-missing))\n (prefetch-components (dedupe all-missing))))))))" "lisp"))
(p "Called once from " (code "boot-init") " after initial processing. Batches all missing deps into one network request. Low priority — browser handles it when idle."))
(~doc-subsection :title "Mouse Approach (Trajectory Prediction)"
(p "Don't wait for the cursor to reach the link — predict where it's heading. Track the last few " (code "mousemove") " events, extrapolate the trajectory, and if it points toward a link, start prefetching before the hover event fires.")
(~doc-code :code (highlight "(define bind-approach-prefetch\n (fn (container)\n ;; Track mouse trajectory within a nav container.\n ;; On each mousemove, extrapolate position ~200ms ahead.\n ;; If projected point intersects a link's bounding box,\n ;; prefetch that link's route deps.\n (let ((last-x 0) (last-y 0) (last-t 0)\n (prefetched (dict)))\n (dom-add-listener container \"mousemove\"\n (fn (e)\n (let ((now (timestamp))\n (dt (- now last-t)))\n (when (> dt 16) ;; ~60fps throttle\n (let ((vx (/ (- (event-x e) last-x) dt))\n (vy (/ (- (event-y e) last-y) dt))\n (px (+ (event-x e) (* vx 200)))\n (py (+ (event-y e) (* vy 200)))\n (target (dom-element-at-point px py)))\n (when (and target (dom-has-attr? target \"href\")\n (not (get prefetched\n (dom-get-attr target \"href\"))))\n (let ((href (dom-get-attr target \"href\")))\n (set! prefetched\n (merge prefetched {href true}))\n (prefetch-route-deps\n (url-pathname href)))))\n (set! last-x (event-x e))\n (set! last-y (event-y e))\n (set! last-t now))))))))" "lisp"))
(p "This is the most speculative strategy — best suited for dense navigation areas (section sidebars, nav bars) where the cursor trajectory is a strong predictor. The " (code "prefetched") " dict prevents duplicate fetches within the same container interaction."))
(~doc-subsection :title "Components + Data (Hybrid Prefetch)"
(p "The most interesting strategy. For pages with " (code ":data") " dependencies, current behavior is full server fallback. But the page's " (em "components") " are still pure and prefetchable. If we prefetch components ahead of time, the click only needs to fetch " (em "data") " — a much smaller, faster response.")
(p "This creates a new rendering path:")
(ol :class "list-decimal pl-5 text-stone-700 space-y-1"
(li "Prefetch: hover/idle/viewport triggers " (code "prefetch-components") " for the target page")
(li "Click: client has components, but page has " (code ":data") " — fetch data from server")
(li "Server returns " (em "only data") " (JSON or SX bindings), not the full rendered page")
(li "Client evaluates the content expression with prefetched components + fetched data")
(li "Result: faster than full server render, no redundant component transfer"))
(~doc-code :code (highlight ";; Declarative: prefetch components, fetch data on click\n(defpage reference-page\n :path \"/reference/<slug>\"\n :auth :public\n :prefetch :components ;; prefetch components, data stays server-fetched\n :data (reference-data slug)\n :content (~reference-attrs-content :attrs attrs))\n\n;; On click, client-side flow:\n;; 1. Components already prefetched (from hover/idle)\n;; 2. GET /reference/attributes → server returns data bindings\n;; 3. Client evals (reference-data slug) result + content expr\n;; 4. Renders locally with cached components" "lisp"))
(p "This is a stepping stone toward full Phase 4 (client IO bridge) of the isomorphic roadmap — it achieves partial client rendering for data pages without needing a general-purpose client async evaluator. The server is a data service, the client is the renderer."))
(~doc-subsection :title "Declarative Configuration"
(p "All strategies configured via " (code "defpage") " metadata and " (code "sx-prefetch") " attributes on links/containers:")
(~doc-code :code (highlight ";; Page-level: what to prefetch for routes linking TO this page\n(defpage docs-page\n :path \"/docs/<slug>\"\n :prefetch :eager) ;; bundle with linking page\n\n(defpage reference-page\n :path \"/reference/<slug>\"\n :prefetch :components) ;; prefetch components, data on click\n\n;; Link-level: override per-link\n(a :href \"/docs/components\"\n :sx-prefetch \"idle\") ;; prefetch after page idle\n\n;; Container-level: approach prediction for nav areas\n(nav :sx-prefetch \"approach\"\n (a :href \"/docs/\") (a :href \"/reference/\") ...)" "lisp"))
(p "Priority cascade: explicit " (code "sx-prefetch") " on link > " (code ":prefetch") " on target defpage > default (hover). The system never prefetches the same components twice — " (code "_prefetch-pending") " and " (code "loaded-component-names") " handle dedup.")))
;; -----------------------------------------------------------------------
;; Design
;; -----------------------------------------------------------------------
(~doc-section :title "Implementation Design" :id "design"
(p "Per the SX host architecture principle: all SX-specific logic goes in " (code ".sx") " spec files and gets bootstrapped. The prefetch logic — scanning links, computing missing deps, managing the component cache — must be specced in " (code ".sx") ", not written directly in JS or Python.")
(~doc-subsection :title "Phase 1: Component Fetch Endpoint (Python)"
(p "A new " (strong "public") " endpoint (not " (code "/internal/") " — the client's browser calls it) that returns component definitions by name.")
(~doc-code :code (highlight "GET /<service-prefix>/sx/components?names=~card,~essay-foo\n\nResponse (text/sx):\n(defcomp ~card (&key title &rest children)\n (div :class \"border rounded p-4\" (h2 title) children))\n(defcomp ~essay-foo (&key id)\n (div (~card :title id)))" "http"))
(p "The server resolves transitive deps via " (code "deps.py") ", subtracts anything listed in the " (code "SX-Components") " request header (already loaded), serializes and returns. This is essentially " (code "components_for_request()") " driven by an explicit " (code "?names=") " param.")
(p "Cache-friendly: the response is a pure function of component hash + requested names. " (code "Cache-Control: public, max-age=3600") " with the component hash as ETag."))
(~doc-subsection :title "Phase 2: Client Prefetch Logic (SX spec)"
(p "New functions in " (code "orchestration.sx") " (or a new " (code "prefetch.sx") " if scope warrants):")
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. compute-missing-deps")
(p "Given a pathname, find the page, return dep names not in " (code "loaded-component-names") ". Returns nil if page not found or has data (can't client-route anyway).")
(~doc-code :code (highlight "(define compute-missing-deps\n (fn (pathname)\n (let ((match (find-matching-route pathname _page-routes)))\n (when (and match (not (get match \"has-data\")))\n (let ((deps (or (get match \"deps\") (list)))\n (loaded (loaded-component-names)))\n (filter (fn (d) (not (contains? loaded d))) deps))))))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "2. prefetch-components")
(p "Fetch component definitions from the server for a list of names. Deduplicates in-flight requests. On success, parses and registers the returned definitions into the component env.")
(~doc-code :code (highlight "(define _prefetch-pending (dict))\n\n(define prefetch-components\n (fn (names)\n (let ((key (join \",\" (sort names))))\n (when (not (get _prefetch-pending key))\n (set! _prefetch-pending\n (merge _prefetch-pending {key true}))\n (fetch-components-from-server names\n (fn (sx-text)\n (sx-process-component-text sx-text)\n (dict-remove! _prefetch-pending key)))))))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "3. prefetch-route-deps")
(p "High-level composition: compute missing deps for a route, fetch if any.")
(~doc-code :code (highlight "(define prefetch-route-deps\n (fn (pathname)\n (let ((missing (compute-missing-deps pathname)))\n (when (and missing (not (empty? missing)))\n (log-info (str \"sx:prefetch \"\n (len missing) \" components for \" pathname))\n (prefetch-components missing)))))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "4. Trigger: link hover")
(p "On mouseover of a boosted link, prefetch its route's missing components. Debounced 150ms to avoid fetching on quick mouse-throughs.")
(~doc-code :code (highlight "(define bind-prefetch-on-hover\n (fn (link)\n (let ((timer nil))\n (dom-add-listener link \"mouseover\"\n (fn (e)\n (clear-timeout timer)\n (set! timer (set-timeout\n (fn () (prefetch-route-deps\n (url-pathname (dom-get-attr link \"href\"))))\n 150))))\n (dom-add-listener link \"mouseout\"\n (fn (e) (clear-timeout timer))))))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "5. Trigger: viewport intersection (opt-in)")
(p "More aggressive strategy: when a link scrolls into view, prefetch its route's deps. Opt-in via " (code "sx-prefetch=\"visible\"") " attribute.")
(~doc-code :code (highlight "(define bind-prefetch-on-visible\n (fn (link)\n (observe-intersection link\n (fn () (prefetch-route-deps\n (url-pathname (dom-get-attr link \"href\"))))\n true 0)))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "6. Integration into process-elements")
(p "During the existing hydration pass, for each boosted link:")
(~doc-code :code (highlight ";; In process-elements, after binding boost behavior:\n(when (and (should-boost-link? link)\n (dom-get-attr link \"href\"))\n (bind-prefetch-on-hover link))\n\n;; Explicit viewport prefetch:\n(when (dom-has-attr? link \"sx-prefetch\")\n (bind-prefetch-on-visible link))" "lisp")))))
(~doc-subsection :title "Phase 3: Boundary Declaration"
(p "Two new IO primitives in " (code "boundary.sx") " (browser-only):")
(~doc-code :code (highlight ";; IO primitives (browser-only)\n(io fetch-components-from-server (names callback) -> void)\n(io sx-process-component-text (sx-text) -> void)" "lisp"))
(p "These are thin wrappers around " (code "fetch()") " + the existing component script processing logic already in the boundary adapter."))
(~doc-subsection :title "Phase 4: Bootstrap"
(p (code "bootstrap_js.py") " picks up the new functions from the spec and emits them into " (code "sx-browser.js") ". The two new boundary IO functions get implemented in the JS boundary adapter — the hand-written glue code that the bootstrapper doesn't generate.")
(~doc-code :code (highlight "// fetch-components-from-server: calls the endpoint\nfunction fetchComponentsFromServer(names, callback) {\n const url = `${routePrefix}/sx/components?names=${names.join(\",\")}`;\n const headers = {\n \"SX-Components\": loadedComponentNames().join(\",\")\n };\n fetch(url, { headers })\n .then(r => r.ok ? r.text() : \"\")\n .then(text => callback(text))\n .catch(() => {}); // silent fail — prefetch is best-effort\n}\n\n// sx-process-component-text: parse defcomp/defmacro into env\nfunction sxProcessComponentText(sxText) {\n if (!sxText) return;\n const frag = document.createElement(\"div\");\n frag.innerHTML =\n `<script type=\"text/sx\" data-components>${sxText}<\\/script>`;\n Sx.processScripts(frag);\n}" "javascript"))))
;; -----------------------------------------------------------------------
;; Request flow
;; -----------------------------------------------------------------------
(~doc-section :title "Request Flow" :id "request-flow"
(p "End-to-end example: user hovers a link, components prefetch, click goes client-side.")
(~doc-code :code (highlight "User hovers link \"/docs/sx-manifesto\"\n |\n +-- bind-prefetch-on-hover fires (150ms debounce)\n |\n +-- compute-missing-deps(\"/docs/sx-manifesto\")\n | +-- find-matching-route -> page with deps:\n | | [\"~essay-sx-manifesto\", \"~doc-code\"]\n | +-- loaded-component-names -> [\"~nav\", \"~footer\", \"~doc-code\"]\n | +-- missing: [\"~essay-sx-manifesto\"]\n |\n +-- prefetch-components([\"~essay-sx-manifesto\"])\n | +-- GET /sx/components?names=~essay-sx-manifesto\n | | Headers: SX-Components: ~nav,~footer,~doc-code\n | +-- Server resolves transitive deps\n | | (also needs ~rich-text, subtracts already-loaded)\n | +-- Response:\n | (defcomp ~essay-sx-manifesto ...) \n | (defcomp ~rich-text ...)\n |\n +-- sx-process-component-text registers defcomps in env\n |\n +-- User clicks link\n +-- try-client-route(\"/docs/sx-manifesto\")\n +-- has-all-deps? -> true (prefetched!)\n +-- eval content -> DOM\n +-- Client-side render, no server roundtrip" "text")))
;; -----------------------------------------------------------------------
;; File changes
;; -----------------------------------------------------------------------
(~doc-section :title "File Changes" :id "file-changes"
(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" "Change")
(th :class "px-3 py-2 font-medium text-stone-600" "Phase")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/helpers.py")
(td :class "px-3 py-2 text-stone-700" "New " (code "sx_components_endpoint()") " route handler")
(td :class "px-3 py-2 text-stone-600" "1"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/infrastructure/factory.py")
(td :class "px-3 py-2 text-stone-700" "Register " (code "/sx/components") " route on all SX apps")
(td :class "px-3 py-2 text-stone-600" "1"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/orchestration.sx")
(td :class "px-3 py-2 text-stone-700" "Prefetch functions: compute-missing-deps, prefetch-components, prefetch-route-deps, bind-prefetch-on-hover, bind-prefetch-on-visible")
(td :class "px-3 py-2 text-stone-600" "2"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/boundary.sx")
(td :class "px-3 py-2 text-stone-700" "Declare " (code "fetch-components-from-server") ", " (code "sx-process-component-text"))
(td :class "px-3 py-2 text-stone-600" "3"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/bootstrap_js.py")
(td :class "px-3 py-2 text-stone-700" "Emit new spec functions, boundary adapter stubs")
(td :class "px-3 py-2 text-stone-600" "4"))))))
;; -----------------------------------------------------------------------
;; Non-goals & rollout
;; -----------------------------------------------------------------------
(~doc-section :title "Non-Goals (This Phase)" :id "non-goals"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Analytics-driven prediction") " — no ML models or click-frequency heuristics. Trajectory prediction uses geometry, not statistics.")
(li (strong "Cross-service prefetch") " — components are per-service. A link to a different service domain is always a server navigation.")
(li (strong "Service worker caching") " — could layer on later, but basic fetch + in-memory registration is sufficient.")
(li (strong "Full client-side data evaluation") " — the components+data strategy fetches data from the server, it doesn't replicate server IO on the client. That's Phase 4 of the isomorphic roadmap.")))
(~doc-section :title "Rollout" :id "rollout"
(p "Incremental, each step independently valuable:")
(ol :class "list-decimal pl-5 text-stone-700 space-y-2"
(li (strong "Component endpoint") " — purely additive. Refactor " (code "components_for_request()") " to accept explicit " (code "?names=") " param.")
(li (strong "Core spec functions") " — " (code "compute-missing-deps") ", " (code "prefetch-components") ", " (code "prefetch-route-deps") " in orchestration.sx. Testable in isolation.")
(li (strong "Hover prefetch") " — wire " (code "bind-prefetch-on-hover") " into " (code "process-elements") ". All boosted links get it automatically. Console logs show activity.")
(li (strong "Idle batch prefetch") " — call " (code "prefetch-visible-links-on-idle") " from " (code "boot-init") ". One request prefetches all visible nav deps after page settles.")
(li (strong "Viewport + approach") " — opt-in via " (code "sx-prefetch") " attributes. Trajectory prediction for dense nav areas.")
(li (strong "Eager bundles") " — extend " (code "components_for_page()") " to include linked routes' deps. Heavier initial payload, zero-latency nav.")
(li (strong "Components + data split") " — new server response mode returning data bindings only. Client renders with prefetched components. Bridges toward Phase 4.")))
(~doc-section :title "Relationship to Isomorphic Roadmap" :id "relationship"
(p "This plan sits between Phase 3 (client-side routing) and Phase 4 (client async & IO bridge) of the "
(a :href "/plans/isomorphic-architecture" :class "text-violet-700 underline" "isomorphic architecture roadmap")
". It extends Phase 3 by making more navigations go client-side without needing any IO bridge — purely by ensuring component definitions are available before they're needed.")
(div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2"
(p :class "text-amber-800 text-sm" (strong "Depends on: ") "Phase 3 (client-side routing with deps checking). No dependency on Phase 4.")))))
;; ---------------------------------------------------------------------------
;; Plan Status Overview
;; ---------------------------------------------------------------------------

View File

@@ -0,0 +1,150 @@
;; ---------------------------------------------------------------------------
;; Reader Macro Demo: #z3 — SX Spec to SMT-LIB (live translation via z3.sx)
;; ---------------------------------------------------------------------------
(defcomp ~z3-example (&key sx-source smt-output)
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
(div
(p :class "text-xs text-stone-500 uppercase tracking-wider mb-1" "SX Source")
(~doc-code :code (highlight sx-source "lisp")))
(div
(p :class "text-xs text-stone-500 uppercase tracking-wider mb-1" "SMT-LIB Output (live from z3.sx)")
(~doc-code :code (highlight smt-output "lisp")))))
(defcomp ~plan-reader-macro-demo-content ()
(~doc-page :title "Reader Macro Demo: #z3"
(~doc-section :title "The idea" :id "idea"
(p :class "text-stone-600"
"SX spec files (" (code "primitives.sx") ", " (code "eval.sx") ") are machine-readable declarations. Reader macros transform these at parse time. " (code "#z3") " reads a " (code "define-primitive") " declaration and emits " (a :href "https://smtlib.cs.uiowa.edu/" :class "text-violet-600 hover:underline" "SMT-LIB") " — the standard input language for " (a :href "https://github.com/Z3Prover/z3" :class "text-violet-600 hover:underline" "Z3") " and other theorem provers.")
(p :class "text-stone-600"
"Same source, two interpretations. The bootstrappers read " (code "define-primitive") " and emit executable code. " (code "#z3") " reads the same form and emits verification conditions. The specification is simultaneously a program and a proof obligation.")
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mt-4"
(p :class "text-sm text-violet-800 font-semibold mb-2" "Self-hosted")
(p :class "text-sm text-violet-700"
"The translator is written in SX itself (" (code "z3.sx") "). The SX evaluator executes " (code "z3.sx") " against the spec to produce SMT-LIB. Same pattern as " (code "bootstrap_js.py") " and " (code "bootstrap_py.py") ", but the transformation logic is an s-expression program transforming other s-expressions. All examples on this page are " (em "live output") " — not hardcoded strings.")))
(~doc-section :title "Arithmetic primitives" :id "arithmetic"
(p :class "text-stone-600"
"Primitives with " (code ":body") " generate " (code "forall") " assertions. Z3 can verify the definition is satisfiable.")
(~doc-subsection :title "inc"
(~z3-example
:sx-source "(define-primitive \"inc\"\n :params (n)\n :returns \"number\"\n :doc \"Increment by 1.\"\n :body (+ n 1))"
:smt-output #z3(define-primitive "inc" :params (n) :returns "number" :doc "Increment by 1." :body (+ n 1))))
(~doc-subsection :title "clamp"
(p :class "text-stone-600 mb-2"
(code "max") " and " (code "min") " have no SMT-LIB equivalent — translated to " (code "ite") " (if-then-else).")
(~z3-example
:sx-source "(define-primitive \"clamp\"\n :params (x lo hi)\n :returns \"number\"\n :doc \"Clamp x to range [lo, hi].\"\n :body (max lo (min hi x)))"
:smt-output #z3(define-primitive "clamp" :params (x lo hi) :returns "number" :doc "Clamp x to range [lo, hi]." :body (max lo (min hi x))))))
(~doc-section :title "Predicates" :id "predicates"
(p :class "text-stone-600"
"Predicates return " (code "Bool") " in SMT-LIB. " (code "mod") " and " (code "=") " are identity translations — same syntax in both languages.")
(~doc-subsection :title "odd?"
(~z3-example
:sx-source "(define-primitive \"odd?\"\n :params (n)\n :returns \"boolean\"\n :doc \"True if n is odd.\"\n :body (= (mod n 2) 1))"
:smt-output #z3(define-primitive "odd?" :params (n) :returns "boolean" :doc "True if n is odd." :body (= (mod n 2) 1))))
(~doc-subsection :title "!= (inequality)"
(~z3-example
:sx-source "(define-primitive \"!=\"\n :params (a b)\n :returns \"boolean\"\n :doc \"Inequality.\"\n :body (not (= a b)))"
:smt-output #z3(define-primitive "!=" :params (a b) :returns "boolean" :doc "Inequality." :body (not (= a b))))))
(~doc-section :title "Variadics and bodyless" :id "variadics"
(p :class "text-stone-600"
"Variadic primitives (" (code "&rest") ") are declared as uninterpreted functions — Z3 can reason about their properties but not their implementation. Primitives without " (code ":body") " get only a declaration.")
(~doc-subsection :title "+ (variadic)"
(~z3-example
:sx-source "(define-primitive \"+\"\n :params (&rest args)\n :returns \"number\"\n :doc \"Sum all arguments.\")"
:smt-output #z3(define-primitive "+" :params (&rest args) :returns "number" :doc "Sum all arguments.")))
(~doc-subsection :title "nil? (no body)"
(~z3-example
:sx-source "(define-primitive \"nil?\"\n :params (x)\n :returns \"boolean\"\n :doc \"True if x is nil/null/None.\")"
:smt-output #z3(define-primitive "nil?" :params (x) :returns "boolean" :doc "True if x is nil/null/None."))))
(~doc-section :title "Translate entire spec files" :id "full-specs"
(p :class "text-stone-600"
"The translator can process entire spec files. " (code "z3-translate-file") " in " (code "z3.sx") " filters for " (code "define-primitive") ", " (code "define-io-primitive") ", and " (code "define-special-form") " declarations, translates each, and concatenates the output.")
(p :class "text-stone-600"
"Below is the live SMT-LIB output from translating the full " (code "primitives.sx") " — all 87 primitive declarations. The composition is pure SX: " (code "(z3-translate-file (sx-parse (read-spec-file \"primitives.sx\")))") " — read the file, parse it, translate it. No Python glue.")
(~doc-subsection :title "primitives.sx (87 primitives)"
(~doc-code :code (highlight (z3-translate-file (sx-parse (read-spec-file "primitives.sx"))) "lisp"))))
(~doc-section :title "The translator: z3.sx" :id "z3-source"
(p :class "text-stone-600"
"The entire translator is a single SX file — s-expressions that walk other s-expressions and emit strings. No host language logic. The same file runs in Python (server) and could run in JavaScript (browser) via the bootstrapped evaluator.")
(~doc-code :code (highlight (read-spec-file "z3.sx") "lisp"))
(p :class "text-stone-600 mt-4"
"359 lines. The key functions: " (code "z3-sort") " maps SX types to SMT-LIB sorts. " (code "z3-expr") " recursively translates expressions — identity ops pass through unchanged, " (code "max") "/" (code "min") " become " (code "ite") ", predicates get renamed. " (code "z3-translate") " dispatches on form type. " (code "z3-translate-file") " filters and batch-translates."))
(~doc-section :title "The pattern: SX → anything" :id "sx-to-anything"
(p :class "text-stone-600"
"z3.sx proves the pattern: an SX program that transforms SX ASTs into a target language. The same pattern works for any target.")
(div :class "rounded border border-stone-200 bg-stone-50 p-4 mt-2 mb-4"
(table :class "w-full text-sm"
(thead (tr :class "text-left text-stone-500"
(th :class "pb-2 pr-4" "File")
(th :class "pb-2 pr-4" "Defines")
(th :class "pb-2 pr-4" "Reader macro")
(th :class "pb-2" "Output")))
(tbody
(tr :class "text-stone-700 border-t border-stone-200"
(td :class "py-2 pr-4 font-mono text-xs" "z3.sx")
(td :class "py-2 pr-4 font-mono text-xs" "z3-translate")
(td :class "py-2 pr-4 font-mono text-xs" "#z3")
(td :class "py-2" "SMT-LIB (theorem prover)"))
(tr :class "text-stone-400 border-t border-stone-200"
(td :class "py-2 pr-4 font-mono text-xs" "py.sx")
(td :class "py-2 pr-4 font-mono text-xs" "py-translate")
(td :class "py-2 pr-4 font-mono text-xs" "#py")
(td :class "py-2" "Python source"))
(tr :class "text-stone-400 border-t border-stone-200"
(td :class "py-2 pr-4 font-mono text-xs" "js.sx")
(td :class "py-2 pr-4 font-mono text-xs" "js-translate")
(td :class "py-2 pr-4 font-mono text-xs" "#js")
(td :class "py-2" "JavaScript source"))
(tr :class "text-stone-400 border-t border-stone-200"
(td :class "py-2 pr-4 font-mono text-xs" "dot.sx")
(td :class "py-2 pr-4 font-mono text-xs" "dot-translate")
(td :class "py-2 pr-4 font-mono text-xs" "#dot")
(td :class "py-2" "Graphviz dot (dependency graphs)")))))
(p :class "text-stone-600"
"Each translator is a pure SX function from AST to string. Drop the " (code ".sx") " file in the ref directory, define " (code "name-translate") ", and " (code "#name") " works everywhere — server and client. No Python. No registration. No glue.")
(p :class "text-stone-600"
"A " (code "py.sx") " wouldn't be limited to the spec. Any SX expression could be translated: " (code "#py(map (fn (x) (* x x)) items)") " → " (code "list(map(lambda x: x * x, items))") ". The bootstrappers (" (code "bootstrap_js.py") ", " (code "bootstrap_py.py") ") are Python programs that do this for the full spec. " (code "py.sx") " would be the same thing, written in SX — a self-hosting bootstrapper."))
(~doc-section :title "How it works" :id "how-it-works"
(p :class "text-stone-600"
"The " (code "#z3") " reader macro is registered before parsing. When the parser hits " (code "#z3(define-primitive ...)") ", it:")
(ol :class "list-decimal pl-6 space-y-2 text-stone-600"
(li "Reads the identifier " (code "z3") " after " (code "#") ".")
(li "Looks for " (code "z3-translate") " in the component environment — found, because " (code "z3.sx") " was loaded at startup.")
(li "Reads the next expression — the full " (code "define-primitive") " form — into an AST node.")
(li "Calls the " (code "z3-translate") " Lambda with the AST.")
(li (code "z3.sx") " walks the AST, extracts " (code ":params") ", " (code ":returns") ", " (code ":body") ", and emits SMT-LIB.")
(li "The resulting string replaces " (code "#z3(...)") " in the parse output."))
(p :class "text-stone-600"
"No Python glue. The parser auto-resolves " (code "#name") " by looking for " (code "name-translate") " in the component env. Any " (code ".sx") " file that defines a " (code "name-translate") " function becomes a reader macro. A future " (code "py.sx") " defining " (code "py-translate") " would make " (code "#py") " work the same way — SX to Python.")
(p :class "text-stone-600"
"The handler is a pure function from AST to value. No side effects. No mutation. Reader macros are " (em "syntax transformations") " — they happen before evaluation, before rendering, before anything else. They are the earliest possible extension point."))
(~doc-section :title "The strange loop" :id "strange-loop"
(p :class "text-stone-600"
"The SX specification files are simultaneously:")
(ul :class "list-disc pl-6 space-y-2 text-stone-600"
(li (span :class "font-semibold" "Executable code") " — bootstrappers compile them to JavaScript and Python.")
(li (span :class "font-semibold" "Documentation") " — this docs site renders them with syntax highlighting and prose.")
(li (span :class "font-semibold" "Formal specifications") " — " (code "#z3") " extracts verification conditions that a theorem prover can check."))
(p :class "text-stone-600"
"One file. Three readings. No information lost. The " (code "define-primitive") " form in " (code "primitives.sx") " does not need to be translated, annotated, or re-expressed for any of these uses. The s-expression " (em "is") " the program, the documentation, and the proof obligation.")
(p :class "text-stone-600"
"And the reader macro that extracts proofs? It is itself written in SX — " (code "z3.sx") ", 359 lines of s-expressions that transform other s-expressions. The SX evaluator (bootstrapped from " (code "eval.sx") ") executes " (code "z3.sx") " against " (code "primitives.sx") " to produce SMT-LIB. The transformation tools are made of the same material as the things they transform.")
(p :class "text-stone-600"
"There is no " (code "reader_z3.py") ". No Python translation logic. The parser sees " (code "#z3") ", finds " (code "z3-translate") " in the component env (loaded from " (code "z3.sx") " at startup), and calls it. The full spec translation calls " (code "(z3-translate-file (sx-parse (read-spec-file ...)))") " — three SX functions composed together. Same evaluator. Same translator. Same source. Different reader."))))

View File

@@ -0,0 +1,110 @@
;; ---------------------------------------------------------------------------
;; Reader Macros
;; ---------------------------------------------------------------------------
(defcomp ~plan-reader-macros-content ()
(~doc-page :title "Reader Macros"
(~doc-section :title "Context" :id "context"
(p "SX has three hardcoded reader transformations: " (code "`") " → " (code "(quasiquote ...)") ", " (code ",") " → " (code "(unquote ...)") ", " (code ",@") " → " (code "(splice-unquote ...)") ". These are baked into the parser with no extensibility. The " (code "~") " prefix for components and " (code "&") " for param modifiers are just symbol characters, handled at eval time.")
(p "Reader macros add parse-time transformations triggered by a dispatch character. Motivating use case: a " (code "~md") " component that uses heredoc syntax for markdown source instead of string literals. More broadly: datum comments, raw strings, and custom literal syntax."))
(~doc-section :title "Design" :id "design"
(~doc-subsection :title "Dispatch Character: #"
(p "Lisp tradition. " (code "#") " is NOT in " (code "ident-start") " or " (code "ident-char") " — completely free. Pattern:")
(~doc-code :code (highlight "#;expr → (read and discard expr, return next)\n#|...| → raw string literal\n#'expr → (quote expr)" "lisp")))
(~doc-subsection :title "#; — Datum comment"
(p "Scheme/Racket standard. Reads and discards the next expression. Preserves balanced parens.")
(~doc-code :code (highlight "(list 1 #;(this is commented out) 2 3) → (list 1 2 3)" "lisp")))
(~doc-subsection :title "#|...| — Raw string"
(p "No escape processing. Everything between " (code "#|") " and " (code "|") " is literal. Enables inline markdown, regex patterns, code examples.")
(~doc-code :code (highlight "(~md #|## Title\n\nSome **bold** text with \"quotes\" and \\backslashes.|)" "lisp")))
(~doc-subsection :title "#' — Quote shorthand"
(p "Currently no single-char quote (" (code "`") " is quasiquote).")
(~doc-code :code (highlight "#'my-function → (quote my-function)" "lisp")))
(~doc-subsection :title "Extensible dispatch: #name"
(p "User-defined reader macros via " (code "#name expr") ". The parser reads an identifier after " (code "#") ", looks up a handler in the reader macro registry, and calls it with the next parsed expression. See the " (a :href "/plans/reader-macro-demo" :class "text-violet-600 hover:underline" "#z3 demo") " for a working example that translates SX spec declarations to SMT-LIB.")))
;; -----------------------------------------------------------------------
;; Implementation
;; -----------------------------------------------------------------------
(~doc-section :title "Implementation" :id "implementation"
(~doc-subsection :title "1. Spec: parser.sx"
(p "Add " (code "#") " dispatch to " (code "read-expr") " (after the " (code ",") "/" (code ",@") " case, before number). Add " (code "read-raw-string") " helper function.")
(~doc-code :code (highlight ";; Reader macro dispatch\n(= ch \"#\")\n (do (set! pos (inc pos))\n (if (>= pos len-src)\n (error \"Unexpected end of input after #\")\n (let ((dispatch-ch (nth source pos)))\n (cond\n ;; #; — datum comment: read and discard next expr\n (= dispatch-ch \";\")\n (do (set! pos (inc pos))\n (read-expr) ;; read and discard\n (read-expr)) ;; return the NEXT expr\n\n ;; #| — raw string\n (= dispatch-ch \"|\")\n (do (set! pos (inc pos))\n (read-raw-string))\n\n ;; #' — quote shorthand\n (= dispatch-ch \"'\")\n (do (set! pos (inc pos))\n (list (make-symbol \"quote\") (read-expr)))\n\n :else\n (error (str \"Unknown reader macro: #\" dispatch-ch))))))" "lisp"))
(p "The " (code "read-raw-string") " helper:")
(~doc-code :code (highlight "(define read-raw-string\n (fn ()\n (let ((buf \"\"))\n (define raw-loop\n (fn ()\n (if (>= pos len-src)\n (error \"Unterminated raw string\")\n (let ((ch (nth source pos)))\n (if (= ch \"|\")\n (do (set! pos (inc pos)) nil) ;; done\n (do (set! buf (str buf ch))\n (set! pos (inc pos))\n (raw-loop)))))))\n (raw-loop)\n buf)))" "lisp")))
(~doc-subsection :title "2. Python: parser.py"
(p "Add " (code "#") " dispatch to " (code "_parse_expr()") " (after " (code ",") "/" (code ",@") " handling ~line 252). Add " (code "_read_raw_string()") " method to Tokenizer.")
(~doc-code :code (highlight "# In _parse_expr(), after the comma/splice-unquote block:\nif raw == \"#\":\n tok._advance(1) # consume the #\n if tok.pos >= len(tok.text):\n raise ParseError(\"Unexpected end of input after #\",\n tok.pos, tok.line, tok.col)\n dispatch = tok.text[tok.pos]\n if dispatch == \";\":\n tok._advance(1)\n _parse_expr(tok) # read and discard\n return _parse_expr(tok) # return next\n if dispatch == \"|\":\n tok._advance(1)\n return tok._read_raw_string()\n if dispatch == \"'\":\n tok._advance(1)\n return [Symbol(\"quote\"), _parse_expr(tok)]\n raise ParseError(f\"Unknown reader macro: #{dispatch}\",\n tok.pos, tok.line, tok.col)" "python"))
(p "The " (code "_read_raw_string()") " method on Tokenizer:")
(~doc-code :code (highlight "def _read_raw_string(self) -> str:\n buf = []\n while self.pos < len(self.text):\n ch = self.text[self.pos]\n if ch == \"|\":\n self._advance(1)\n return \"\".join(buf)\n buf.append(ch)\n self._advance(1)\n raise ParseError(\"Unterminated raw string\",\n self.pos, self.line, self.col)" "python")))
(~doc-subsection :title "3. JS: auto-transpiled"
(p "JS parser comes from bootstrap of parser.sx — spec change handles it automatically."))
(~doc-subsection :title "4. Rebootstrap both targets"
(p "Run " (code "bootstrap_js.py") " and " (code "bootstrap_py.py") " to regenerate " (code "sx-ref.js") " and " (code "sx_ref.py") " from the updated parser.sx spec."))
(~doc-subsection :title "5. Grammar update"
(p "Add reader macro syntax to the grammar comment at the top of parser.sx:")
(~doc-code :code (highlight ";; reader → '#;' expr (datum comment)\n;; | '#|' raw-chars '|' (raw string)\n;; | \"#'\" expr (quote shorthand)" "lisp"))))
;; -----------------------------------------------------------------------
;; Files
;; -----------------------------------------------------------------------
(~doc-section :title "Files" :id "files"
(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" "Change")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/parser.sx")
(td :class "px-3 py-2 text-stone-700" "# dispatch in read-expr, read-raw-string helper, grammar comment"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/parser.py")
(td :class "px-3 py-2 text-stone-700" "# dispatch in _parse_expr(), _read_raw_string()"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/sx_ref.py")
(td :class "px-3 py-2 text-stone-700" "Rebootstrap"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/static/scripts/sx-ref.js")
(td :class "px-3 py-2 text-stone-700" "Rebootstrap"))))))
;; -----------------------------------------------------------------------
;; Verification
;; -----------------------------------------------------------------------
(~doc-section :title "Verification" :id "verification"
(~doc-subsection :title "Parse tests"
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "#;(ignored) 42 → 42")
(li "(list 1 #;2 3) → (list 1 3)")
(li "#|hello \"world\" \\n| → string: hello \"world\" \\n (literal, no escaping)")
(li "#|multi\\nline| → string with actual newline")
(li "#'foo → (quote foo)")
(li "# at EOF → error")
(li "#x unknown → error")))
(~doc-subsection :title "Regression"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "All existing parser tests pass after changes")
(li "Rebootstrapped JS and Python pass their test suites")
(li "JS parity: SX.parse('#|hello|') returns [\"hello\"]"))))))
;; ---------------------------------------------------------------------------
;; Reader Macro Demo: #z3 — SX Spec to SMT-LIB
;; ---------------------------------------------------------------------------

View File

@@ -0,0 +1,62 @@
;; ---------------------------------------------------------------------------
;; Social Sharing
;; ---------------------------------------------------------------------------
(defcomp ~plan-social-sharing-content ()
(~doc-page :title "Social Network Sharing"
(~doc-section :title "Context" :id "context"
(p "Rose Ash already has ActivityPub for federated social sharing. This plan adds OAuth-based sharing to mainstream social networks — Facebook, Instagram, Threads, Twitter/X, LinkedIn, and Mastodon.")
(p "All social logic lives in the " (strong "account") " microservice. Content apps get a share button that opens the account share page."))
(~doc-section :title "What remains" :id "remains"
(~doc-note "Nothing has been implemented. This is the full scope of work.")
(div :class "space-y-4"
(div :class "rounded border border-stone-200 p-4"
(h4 :class "font-semibold text-stone-700 mb-2" "Phase 1: Data Model + Encryption")
(ul :class "list-disc pl-5 text-stone-700 space-y-1 text-sm"
(li (code "shared/models/social_connection.py") " — SocialConnection model (user_id, platform, tokens, scopes, extra_data)")
(li (code "shared/infrastructure/social_crypto.py") " — Fernet encrypt/decrypt for tokens")
(li "Alembic migration for social_connections table")
(li "Environment variables for per-platform OAuth credentials")))
(div :class "rounded border border-stone-200 p-4"
(h4 :class "font-semibold text-stone-700 mb-2" "Phase 2: Platform OAuth Clients")
(p :class "text-sm text-stone-600 mb-2" "All in " (code "account/services/social_platforms/") ":")
(ul :class "list-disc pl-5 text-stone-700 space-y-1 text-sm"
(li (code "base.py") " — SocialPlatform ABC, OAuthResult, ShareResult")
(li (code "meta.py") " — Facebook + Instagram + Threads (Graph API)")
(li (code "twitter.py") " — OAuth 2.0 with PKCE")
(li (code "linkedin.py") " — LinkedIn Posts API")
(li (code "mastodon.py") " — Dynamic app registration per instance")))
(div :class "rounded border border-stone-200 p-4"
(h4 :class "font-semibold text-stone-700 mb-2" "Phase 3: Account Blueprint")
(ul :class "list-disc pl-5 text-stone-700 space-y-1 text-sm"
(li (code "account/bp/social/routes.py") " — /social/ list, /social/connect/<platform>/, /social/callback/<platform>/, /social/share/")
(li "Register before account blueprint (account has catch-all /<slug>/ route)")))
(div :class "rounded border border-stone-200 p-4"
(h4 :class "font-semibold text-stone-700 mb-2" "Phase 4: Templates")
(ul :class "list-disc pl-5 text-stone-700 space-y-1 text-sm"
(li "Social panel — platform cards, connect/disconnect")
(li "Share panel — content preview, account checkboxes, share button")
(li "Share result — per-platform success/failure with links")))
(div :class "rounded border border-stone-200 p-4"
(h4 :class "font-semibold text-stone-700 mb-2" "Phase 5: Share Button in Content Apps")
(ul :class "list-disc pl-5 text-stone-700 space-y-1 text-sm"
(li "share-button fragment from account service")
(li "Blog, events, market detail pages fetch and render the fragment")))
(div :class "rounded border border-stone-200 p-4"
(h4 :class "font-semibold text-stone-700 mb-2" "Phase 6: Token Refresh + Share History")
(ul :class "list-disc pl-5 text-stone-700 space-y-1 text-sm"
(li "Automatic token refresh before posting")
(li "Optional social_shares table for history and duplicate prevention")))))))
;; ---------------------------------------------------------------------------
;; Isomorphic Architecture Roadmap
;; ---------------------------------------------------------------------------

139
sx/sx/plans/status.sx Normal file
View File

@@ -0,0 +1,139 @@
;; ---------------------------------------------------------------------------
;; Plan Status Overview
;; ---------------------------------------------------------------------------
(defcomp ~plan-status-content ()
(~doc-page :title "Plan Status"
(p :class "text-lg text-stone-600 mb-6"
"Audit of all plans across the SX language and Rose Ash platform. Last updated March 2026.")
;; -----------------------------------------------------------------------
;; Completed
;; -----------------------------------------------------------------------
(~doc-section :title "Completed" :id "completed"
(div :class "space-y-4"
(div :class "rounded border border-green-200 bg-green-50 p-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
(span :class "font-semibold text-stone-800" "Split Cart into Microservices"))
(p :class "text-sm text-stone-600" "Cart decomposed into 4 services: relations (internal, owns ContainerRelation), likes (internal, unified generic likes), orders (public, owns Order/OrderItem + SumUp checkout), and cart (thin CartItem CRUD). All three new services deployed with dedicated databases."))
(div :class "rounded border border-green-200 bg-green-50 p-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
(span :class "font-semibold text-stone-800" "Ticket Purchase Through Cart"))
(p :class "text-sm text-stone-600" "Tickets flow through the cart like products: state=pending in cart, reserved at checkout, confirmed on payment. TicketDTO, CartSummaryDTO with ticket_count/ticket_total, CalendarService protocol methods all implemented."))
(div :class "rounded border border-green-200 bg-green-50 p-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
(span :class "font-semibold text-stone-800" "Ticket UX Improvements"))
(p :class "text-sm text-stone-600" "+/- quantity buttons on entry pages and cart. Tickets grouped by event in cart display. Adjust quantity route, sold/basket counts, matching product card UX pattern."))
(div :class "rounded border border-green-200 bg-green-50 p-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
(a :href "/isomorphism/" :class "font-semibold text-green-800 underline" "Isomorphic Phase 1: Dependency Analysis"))
(p :class "text-sm text-stone-600" "Per-page component bundles via deps.sx. Transitive closure, scan-refs, components-needed, page-css-classes. 15 tests, bootstrapped to both hosts."))
(div :class "rounded border border-green-200 bg-green-50 p-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
(a :href "/isomorphism/" :class "font-semibold text-green-800 underline" "Isomorphic Phase 2: IO Detection"))
(p :class "text-sm text-stone-600" "Automatic IO classification. scan-io-refs, transitive-io-refs, compute-all-io-refs. Server expands IO components, serializes pure ones for client."))
(div :class "rounded border border-green-200 bg-green-50 p-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
(a :href "/isomorphism/" :class "font-semibold text-green-800 underline" "Isomorphic Phase 3: Client-Side Routing"))
(p :class "text-sm text-stone-600" "router.sx spec, page registry via <script type=\"text/sx-pages\">, client route matching, try-first/fallback to server. Pure pages render without server roundtrips."))
(div :class "rounded border border-green-200 bg-green-50 p-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
(a :href "/isomorphism/" :class "font-semibold text-green-800 underline" "Isomorphic Phase 4: Client Async & IO Bridge"))
(p :class "text-sm text-stone-600" "Server evaluates :data expressions, serializes as SX wire format. Client fetches pre-evaluated data, caches with 30s TTL, renders :content locally. 30 unit tests."))
(div :class "rounded border border-green-200 bg-green-50 p-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
(a :href "/isomorphism/" :class "font-semibold text-green-800 underline" "Isomorphic Phase 5: Client IO Proxy"))
(p :class "text-sm text-stone-600" "IO primitives (highlight, asset-url, etc.) proxied to server via registerIoDeps(). Async DOM renderer handles promises through the render tree. Components with IO deps render client-side via server round-trips — no continuations needed."))
(div :class "rounded border border-green-200 bg-green-50 p-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
(a :href "/testing/" :class "font-semibold text-green-800 underline" "Modular Test Architecture"))
(p :class "text-sm text-stone-600" "Per-module test specs (eval, parser, router, render) with 161 tests. Three runners: Python, Node.js, browser. 5 platform functions, everything else pure SX."))))
;; -----------------------------------------------------------------------
;; In Progress / Partial
;; -----------------------------------------------------------------------
(~doc-section :title "In Progress" :id "in-progress"
(div :class "space-y-4"
(div :class "rounded border border-amber-200 bg-amber-50 p-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-amber-600 text-white uppercase" "Partial")
(a :href "/plans/fragment-protocol" :class "font-semibold text-amber-900 underline" "Fragment Protocol"))
(p :class "text-sm text-stone-600" "Fragment GET infrastructure works. The planned POST/sexp structured protocol for transferring component definitions between services is not yet implemented. Fragment endpoints still use legacy GET + X-Fragment-Request headers."))))
;; -----------------------------------------------------------------------
;; Not Started
;; -----------------------------------------------------------------------
(~doc-section :title "Not Started" :id "not-started"
(div :class "space-y-4"
(div :class "rounded border border-stone-200 bg-stone-50 p-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-700 text-white uppercase" "Done")
(a :href "/plans/reader-macros" :class "font-semibold text-stone-800 underline" "Reader Macros"))
(p :class "text-sm text-stone-600" "# dispatch in parser.sx spec, Python parser.py, hand-written sx.js. Three built-ins (#;, #|...|, #') plus extensible #name dispatch. #z3 demo translates define-primitive to SMT-LIB.")
(p :class "text-sm text-stone-500 mt-1" "48 parser tests (SX + Python), all passing. Rebootstrapped to JS and Python."))
(div :class "rounded border border-stone-200 bg-stone-50 p-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-stone-500 text-white uppercase" "Not Started")
(a :href "/plans/sx-activity" :class "font-semibold text-stone-800 underline" "SX-Activity"))
(p :class "text-sm text-stone-600" "Federated SX over ActivityPub — 6 phases from SX wire format for activities to the evaluable web on IPFS. Existing AP infrastructure provides the foundation but no SX-specific federation code exists.")
(p :class "text-sm text-stone-500 mt-1" "Remaining: shared/sx/activity.py (SX<->JSON-LD), shared/sx/ipfs.py, shared/sx/ref/ipfs-resolve.sx, shared/sx/registry.py, shared/sx/anchor.py."))
(div :class "rounded border border-stone-200 bg-stone-50 p-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-stone-500 text-white uppercase" "Not Started")
(a :href "/plans/glue-decoupling" :class "font-semibold text-stone-800 underline" "Cross-App Decoupling via Glue"))
(p :class "text-sm text-stone-600" "Eliminate all cross-app model imports by routing through a glue service layer. No glue/ directory exists. Apps are currently decoupled via HTTP interfaces and DTOs instead.")
(p :class "text-sm text-stone-500 mt-1" "Remaining: glue/services/ for pages, page_config, calendars, marketplaces, cart_items, products, post_associations. 25+ cross-app imports to eliminate."))
(div :class "rounded border border-stone-200 bg-stone-50 p-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-stone-500 text-white uppercase" "Not Started")
(a :href "/plans/social-sharing" :class "font-semibold text-stone-800 underline" "Social Network Sharing"))
(p :class "text-sm text-stone-600" "OAuth-based sharing to Facebook, Instagram, Threads, Twitter/X, LinkedIn, and Mastodon via the account service. No models, blueprints, or platform clients created.")
(p :class "text-sm text-stone-500 mt-1" "Remaining: SocialConnection model, social_crypto.py, platform OAuth clients (6), account/bp/social/ blueprint, share button fragment."))
(div :class "rounded border border-green-200 bg-green-50 p-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
(a :href "/isomorphism/" :class "font-semibold text-stone-800 underline" "Isomorphic Phase 6: Streaming & Suspense"))
(p :class "text-sm text-stone-600" "Server streams partially-evaluated SX as IO resolves. ~suspense component renders fallbacks, inline resolution scripts fill in content. Concurrent IO via asyncio, chunked transfer encoding.")
(p :class "text-sm text-stone-500 mt-1" "Demo: " (a :href "/isomorphism/streaming" "/isomorphism/streaming")))
(div :class "rounded border border-green-300 bg-green-50 p-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
(a :href "/isomorphism/" :class "font-semibold text-stone-800 underline" "Isomorphic Phase 7: Full Isomorphism"))
(p :class "text-sm text-stone-600" "Affinity annotations, render plans, optimistic data updates, offline mutation queue, isomorphic testing harness, universal page descriptor.")
(p :class "text-sm text-stone-500 mt-1" "All 6 sub-phases (7a7f) complete."))))))
;; ---------------------------------------------------------------------------
;; Fragment Protocol
;; ---------------------------------------------------------------------------

471
sx/sx/plans/sx-activity.sx Normal file
View File

@@ -0,0 +1,471 @@
;; ---------------------------------------------------------------------------
;; SX-Activity: Federated SX over ActivityPub
;; ---------------------------------------------------------------------------
(defcomp ~plan-sx-activity-content ()
(~doc-page :title "SX-Activity"
(~doc-section :title "Context" :id "context"
(p "The web is six incompatible formats duct-taped together: HTML for structure, CSS for style, JavaScript for behavior, JSON for data, server languages for backend logic, build tools for compilation. Moving anything between layers requires serialization, template languages, API contracts, and glue code. Federation (ActivityPub) adds a seventh — JSON-LD — which is inert data that every consumer must interpret from scratch and wrap in their own UI.")
(p "SX is already one evaluable format that does all six. A component definition is simultaneously structure, style (components apply classes and respond to data), behavior (event handlers), data (the AST " (em "is") " data), server-renderable (Python evaluator), and client-renderable (JS evaluator). The pieces already exist: content-addressed DAG execution (artdag), IPFS storage with CIDs, OpenTimestamps Bitcoin anchoring, boundary-enforced sandboxing.")
(p "SX-Activity wires these together into a new web. Everything — content, UI components, markdown parsers, syntax highlighters, validation logic, media, processing pipelines — is the same executable format, stored on a content-addressed network, running within each participant's own security context. " (strong "The wire format is the programming language is the component system is the package manager.")))
(~doc-section :title "Current State" :id "current-state"
(ul :class "space-y-2 text-stone-700 list-disc pl-5"
(li (strong "ActivityPub: ") "Full implementation — virtual per-app actors, HTTP signatures, webfinger, inbox/outbox, followers/following, delivery with idempotent logging.")
(li (strong "Activity bus: ") "Unified event bus with NOTIFY/LISTEN wakeup, at-least-once delivery, handler registry keyed by (activity_type, object_type).")
(li (strong "Content addressing: ") "artdag nodes use SHA3-256 hashing. Cache layer tracks IPFS CIDs. IPFSPin model tracks pinned content across domains.")
(li (strong "Bitcoin anchoring: ") "APAnchor model — Merkle tree of activities, OpenTimestamps proof CID, Bitcoin txid. Infrastructure exists but isn't wired to all activity types.")
(li (strong "SX wire format: ") "Server serializes to SX source via _aser, client parses and renders. Component caching via localStorage + content hashes.")
(li (strong "Boundary enforcement: ") "SX_BOUNDARY_STRICT=1 validates all primitives at registration. Pure components can't do IO — safe to load from untrusted sources.")))
;; -----------------------------------------------------------------------
;; Phase 1: SX Wire Format for Activities
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 1: SX Wire Format for Activities" :id "phase-1"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "Activities expressed as s-expressions instead of JSON-LD. Same semantics as ActivityStreams, but compact, parseable, and directly evaluable. Dual-format support for backward compatibility with existing AP servers."))
(~doc-subsection :title "The Problem"
(p "JSON-LD activities are verbose and require context resolution:")
(~doc-code :code (highlight "{\"@context\": \"https://www.w3.org/ns/activitystreams\",\n \"type\": \"Create\",\n \"actor\": \"https://example.com/users/alice\",\n \"object\": {\n \"type\": \"Note\",\n \"content\": \"<p>Hello world</p>\",\n \"attributedTo\": \"https://example.com/users/alice\"\n }}" "json"))
(p "Every consumer parses JSON, resolves @context, extracts fields, then builds their own UI around the raw data. The content is HTML embedded in a JSON string — two formats nested, neither evaluable."))
(~doc-subsection :title "SX Activity Format"
(p "The same activity as SX:")
(~doc-code :code (highlight "(Create\n :actor \"https://example.com/users/alice\"\n :published \"2026-03-06T12:00:00Z\"\n :object (Note\n :attributed-to \"https://example.com/users/alice\"\n :content (p \"Hello world\")\n :media-type \"text/sx\"))" "lisp"))
(p "The content isn't a string containing markup — it " (em "is") " markup. The receiving server can evaluate it directly. The Note's content is a renderable SX expression."))
(~doc-subsection :title "Approach"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Activity vocabulary in SX")
(p "Map ActivityStreams types to SX symbols. Activities are lists with a type head and keyword properties:")
(~doc-code :code (highlight ";; Core activity types\n(Create :actor ... :object ...)\n(Update :actor ... :object ...)\n(Delete :actor ... :object ...)\n(Follow :actor ... :object ...)\n(Like :actor ... :object ...)\n(Announce :actor ... :object ...)\n\n;; Object types\n(Note :content ... :attributed-to ...)\n(Article :name ... :content ... :summary ...)\n(Image :url ... :media-type ... :cid ...)\n(Collection :total-items ... :items ...)" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "2. Content negotiation")
(p "Inbox accepts both formats. " (code "Accept: text/sx") " gets SX, " (code "Accept: application/activity+json") " gets JSON-LD. Outbox serves both. SX-native servers negotiate SX; legacy Mastodon/Pleroma servers get JSON-LD as today."))
(div
(h4 :class "font-semibold text-stone-700" "3. Bidirectional translation")
(p "Lossless mapping between JSON-LD and SX activity formats. Translate at the boundary — internal processing always uses SX. The existing " (code "APActivity") " model gains an " (code "sx_source") " column storing the canonical SX representation."))
(div
(h4 :class "font-semibold text-stone-700" "4. HTTP Signatures over SX")
(p "Same RSA signature mechanism. Digest header computed over the SX body. Existing keypair infrastructure unchanged."))))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Round-trip: SX → JSON-LD → SX produces identical output")
(li "Legacy AP servers receive valid JSON-LD (Mastodon can display the post)")
(li "SX-native servers receive evaluable SX (client can render directly)")
(li "HTTP signatures verify over SX bodies"))))
;; -----------------------------------------------------------------------
;; Phase 2: Content-Addressed Components on IPFS
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 2: Content-Addressed Components on IPFS" :id "phase-2"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "Component definitions stored on IPFS, referenced by CID. Any server can publish components. Any browser can fetch them. No central registry — content addressing IS the registry."))
(~doc-subsection :title "The Insight"
(p "SX components are pure functions — they take data and return markup. They can't do IO (boundary enforcement guarantees this). That means they're " (strong "safe to load from any source") ". And if they're content-addressed, the CID " (em "is") " the identity — you don't need to trust the source, you just verify the hash.")
(p "Currently, component definitions travel with each page via " (code "<script type=\"text/sx\" data-components>") ". Each server bundles its own. With IPFS, components become shared infrastructure — define once, use everywhere."))
(~doc-subsection :title "Approach"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Component CID computation")
(p "Each " (code "defcomp") " definition gets a content address:")
(~doc-code :code (highlight ";; Component source\n(defcomp ~card (&key title &rest children)\n (div :class \"border rounded p-4\"\n (h2 title) children))\n\n;; CID = SHA3-256 of canonical serialized form\n;; → bafy...abc123\n;; Stored: ipfs://bafy...abc123 → component source" "lisp"))
(p "Canonical form: normalize whitespace, sort keyword args alphabetically, strip comments. Same component always produces same CID regardless of formatting."))
(div
(h4 :class "font-semibold text-stone-700" "2. Component references in activities")
(p "Activities declare which components they need by CID:")
(~doc-code :code (highlight "(Create\n :actor \"https://rose-ash.com/users/alice\"\n :requires (list\n \"bafy...card\" ;; ~card component\n \"bafy...avatar\") ;; ~avatar component\n :object (Note\n :content (~card :title \"Hello\"\n (~avatar :src \"ipfs://bafy...photo\")\n (p \"This renders with the card component.\"))))" "lisp"))
(p "The receiving browser fetches missing components from IPFS, verifies CIDs, registers them, then renders the content."))
(div
(h4 :class "font-semibold text-stone-700" "3. IPFS component resolution")
(p "Client-side resolution pipeline:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Check localStorage cache (keyed by CID — cache-forever semantics)")
(li "Check local IPFS node if running (ipfs cat)")
(li "Fetch from IPFS gateway (configurable, default: dweb.link)")
(li "Verify SHA3-256 matches CID")
(li "Parse, register in component env, render")))
(div
(h4 :class "font-semibold text-stone-700" "4. Component publication")
(p "Server-side: on component registration, compute CID and pin to IPFS. Track in " (code "IPFSPin") " model (already exists). Publish component availability via AP outbox:")
(~doc-code :code (highlight "(Create\n :actor \"https://rose-ash.com/apps/market\"\n :object (SxComponent\n :name \"~product-card\"\n :cid \"bafy...productcard\"\n :version \"1.0.0\"\n :deps (list \"bafy...card\" \"bafy...price-tag\")))" "lisp")))))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Component pinned to IPFS → fetchable via gateway → CID verifies")
(li "Browser renders federated post using IPFS-fetched components")
(li "Modified component → different CID → old content still renders with old version")
(li "Boundary enforcement: IPFS-loaded component cannot call IO primitives"))))
;; -----------------------------------------------------------------------
;; Phase 3: Federated Media & Content Store
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 3: Federated Media & Content Store" :id "phase-3"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "All media (images, video, audio, DAG outputs) stored content-addressed on IPFS. Activities reference media by CID. No hotlinking, no broken links, no dependence on the origin server staying online."))
(~doc-subsection :title "Current Mechanism"
(p "artdag already content-addresses all DAG outputs with SHA3-256 and tracks IPFS CIDs in " (code "IPFSPin") ". But media in the web platform (blog images, product photos, event banners) is stored as regular files on the origin server. Federated posts include " (code "url") " fields pointing to the origin — if the server goes down, the media is gone."))
(~doc-subsection :title "Approach"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Media CID pipeline")
(p "On upload: hash content → pin to IPFS → store CID in database. Activities reference media by CID alongside URL fallback:")
(~doc-code :code (highlight "(Image\n :cid \"bafy...photo123\"\n :url \"https://rose-ash.com/media/photo.jpg\" ;; fallback\n :media-type \"image/jpeg\"\n :width 1200 :height 800)" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "2. DAG output federation")
(p "artdag processing results (rendered video, processed images) already have CIDs. Federate them as activities:")
(~doc-code :code (highlight "(Create\n :actor \"https://rose-ash.com/users/alice\"\n :object (Artwork\n :name \"Sunset Remix\"\n :cid \"bafy...artwork\"\n :dag-cid \"bafy...dag\" ;; full DAG for reproduction\n :media-type \"video/mp4\"\n :sources (list\n (Image :cid \"bafy...src1\" :attribution \"...\")\n (Image :cid \"bafy...src2\" :attribution \"...\"))))" "lisp"))
(p "The " (code ":dag-cid") " lets anyone re-execute the processing pipeline. The artwork is both a result and a reproducible recipe."))
(div
(h4 :class "font-semibold text-stone-700" "3. Shared SX content store")
(p "Not just components and media — full page content can be content-addressed. An Article's body is SX, pinned to IPFS:")
(~doc-code :code (highlight "(Article\n :name \"Why S-Expressions\"\n :content-cid \"bafy...article-body\" ;; SX source on IPFS\n :requires (list \"bafy...doc-page\" \"bafy...code-block\")\n :summary \"Why SX uses s-expressions instead of HTML.\")" "lisp"))
(p "The content outlives the server. Anyone with the CID can fetch, parse, and render the article with its original components."))
(div
(h4 :class "font-semibold text-stone-700" "4. Progressive resolution")
(p "Client resolves content progressively:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Inline content renders immediately")
(li "CID-referenced content shows placeholder → fetches from IPFS → renders")
(li "Large media uses IPFS streaming (chunked CIDs)")
(li "Integrates with Phase 6 of isomorphic plan (streaming/suspense)")))))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Origin server offline → content still resolvable via IPFS gateway")
(li "DAG CID → re-executing DAG produces identical output")
(li "Media CID verifies → tampered content rejected"))))
;; -----------------------------------------------------------------------
;; Phase 4: Component Registry & Discovery
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 4: Component Registry & Discovery" :id "phase-4"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "Federated component discovery. Servers publish component collections. Other servers follow component feeds. Like npm, but federated, content-addressed, and the packages are safe to run (pure functions, no IO)."))
(~doc-subsection :title "Approach"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Component collections as AP actors")
(p "Each server exposes a component registry actor:")
(~doc-code :code (highlight "(Service\n :id \"https://rose-ash.com/sx-registry\"\n :type \"SxComponentRegistry\"\n :name \"Rose Ash Components\"\n :outbox \"https://rose-ash.com/sx-registry/outbox\"\n :followers \"https://rose-ash.com/sx-registry/followers\")" "lisp"))
(p "Follow the registry to receive component updates. The outbox is a chronological feed of Create/Update/Delete activities for components."))
(div
(h4 :class "font-semibold text-stone-700" "2. Component metadata")
(~doc-code :code (highlight "(SxComponent\n :name \"~data-table\"\n :cid \"bafy...datatable\"\n :version \"2.1.0\"\n :deps (list \"bafy...sortable\" \"bafy...paginator\")\n :params (list\n (dict :name \"rows\" :type \"list\" :required true)\n (dict :name \"columns\" :type \"list\" :required true)\n (dict :name \"sortable\" :type \"boolean\" :default false))\n :css-atoms (list :border :rounded :p-4 :text-sm)\n :preview-cid \"bafy...screenshot\"\n :license \"MIT\")" "lisp"))
(p "Dependencies are transitive CID references. CSS atoms declare which CSSX rules the component needs. Preview CID is a screenshot for registry browsing."))
(div
(h4 :class "font-semibold text-stone-700" "3. Discovery protocol")
(p "Webfinger-style lookup for components by name:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Local registry: check own component env first")
(li "Followed registries: check cached feeds from followed registries")
(li "Global search: query known registries by component name")
(li "CID resolution: if you have a CID, skip discovery — fetch directly from IPFS")))
(div
(h4 :class "font-semibold text-stone-700" "4. Version resolution")
(p "Components are immutable (CID = identity). \"Updating\" a component publishes a new CID. Activities reference specific CIDs, so old content always renders correctly. The registry tracks version history:")
(~doc-code :code (highlight "(Update\n :actor \"https://rose-ash.com/sx-registry\"\n :object (SxComponent\n :name \"~card\"\n :cid \"bafy...card-v2\" ;; new version\n :replaces \"bafy...card-v1\" ;; previous version\n :changelog \"Added subtitle slot\"))" "lisp")))))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Follow registry → receive component Create activities → components available locally")
(li "Render post using component from foreign registry → works")
(li "Old post referencing old CID → still renders correctly with old version"))))
;; -----------------------------------------------------------------------
;; Phase 5: Bitcoin-Anchored Provenance
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 5: Bitcoin-Anchored Provenance" :id "phase-5"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "Cryptographic proof that content existed at a specific time, authored by a specific actor. Leverages the existing APAnchor/OpenTimestamps infrastructure. Unforgeable, independently verifiable, survives server shutdown."))
(~doc-subsection :title "Current Mechanism"
(p "The " (code "APAnchor") " model already batches activities into Merkle trees, stores the tree on IPFS, creates an OpenTimestamps proof, and records the Bitcoin txid. This runs but isn't surfaced to users or integrated with the full activity lifecycle."))
(~doc-subsection :title "Approach"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Automatic anchoring pipeline")
(p "Every SX activity gets queued for anchoring. Batch processor runs periodically:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Collect pending activities (content CIDs + actor signatures)")
(li "Build Merkle tree of activity hashes")
(li "Pin Merkle tree to IPFS → tree CID")
(li "Submit tree root to OpenTimestamps calendar servers")
(li "When Bitcoin confirmation arrives: store txid, update activities with anchor reference")))
(div
(h4 :class "font-semibold text-stone-700" "2. Provenance chain in activities")
(~doc-code :code (highlight "(Create\n :actor \"https://rose-ash.com/users/alice\"\n :object (Note :content (p \"Hello\") :cid \"bafy...note\")\n :provenance (Anchor\n :tree-cid \"bafy...merkle-tree\"\n :leaf-index 42\n :ots-cid \"bafy...ots-proof\"\n :btc-txid \"abc123...def\"\n :btc-block 890123\n :anchored-at \"2026-03-06T12:00:00Z\"))" "lisp"))
(p "Any party can verify: fetch the OTS proof from IPFS, check the Merkle path from the activity's CID to the tree root, confirm the tree root is committed in the Bitcoin block."))
(div
(h4 :class "font-semibold text-stone-700" "3. Component provenance")
(p "Components published to the registry also get anchored. This proves authorship and publication time:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "\"This component was published by rose-ash.com at block 890123\"")
(li "Prevents backdating — can't claim you published a component before you actually did")
(li "License disputes resolvable by checking anchor timestamps")))
(div
(h4 :class "font-semibold text-stone-700" "4. Verification UI")
(p "Client-side provenance badge on federated content:")
(~doc-code :code (highlight "(defcomp ~provenance-badge (&key anchor)\n (when anchor\n (details :class \"inline text-xs text-stone-400\"\n (summary \"✓ Anchored\")\n (dl :class \"mt-1 space-y-1\"\n (dt \"Bitcoin block\") (dd (get anchor \"btc-block\"))\n (dt \"Timestamp\") (dd (get anchor \"anchored-at\"))\n (dt \"Proof\") (dd (a :href (str \"ipfs://\" (get anchor \"ots-cid\"))\n \"OTS proof\"))))))" "lisp")))))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Activity anchored → OTS proof fetchable from IPFS → Merkle path validates → txid confirms in Bitcoin")
(li "Tampered activity → Merkle proof fails → provenance badge shows ✗")
(li "Server goes offline → provenance still verifiable (all proofs on IPFS + Bitcoin)"))))
;; -----------------------------------------------------------------------
;; Phase 6: The Evaluable Web
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 6: The Evaluable Web" :id "phase-6"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What this really is")
(p :class "text-violet-800" "Not ActivityPub-with-SX. A new web. One where everything — content, components, parsers, renderers, server logic, client logic — is the same executable format, shared on a content-addressed network, running within each participant's own security context."))
(~doc-subsection :title "The insight"
(p "The web has six layers that don't talk to each other: HTML (structure), CSS (style), JavaScript (behavior), JSON (data interchange), server frameworks (backend logic), and build tools (compilation). Each has its own syntax, its own semantics, its own ecosystem. Moving data between them requires serialization, deserialization, template languages, API contracts, type coercion, and an endless parade of glue code.")
(p "SX collapses all six into one evaluable format. A component definition is simultaneously structure, style (components apply classes and respond to data), behavior (event handlers), data (the AST is data), server-renderable (Python evaluator), and client-renderable (JS evaluator). There is no boundary between \"data\" and \"program\" — s-expressions are both.")
(p "Once that's true, " (strong "everything becomes shareable.") " Not just UI components — markdown parsers, syntax highlighters, date formatters, validation logic, layout algorithms, color systems, animation curves. Any pure function over data. All content-addressed, all on IPFS, all executable within your own security context."))
(~doc-subsection :title "What travels on the network"
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "Content")
(p "Blog posts, product listings, event descriptions, social media posts. Not HTML strings embedded in JSON — live SX expressions that evaluate to rendered UI. The content " (em "is") " the application."))
(div
(h4 :class "font-semibold text-stone-700" "Components")
(p "UI building blocks: cards, tables, forms, navigation, media players. Published to IPFS, referenced by CID. A commerce site publishes " (code "~product-card") ". A blogging platform publishes " (code "~article-layout") ". A social network publishes " (code "~thread-view") ". Anyone can compose them. They're pure functions — safe to load from anywhere."))
(div
(h4 :class "font-semibold text-stone-700" "Parsers and transforms")
(p "A markdown parser is just an SX function: takes a string, returns an SX tree. Publish it to IPFS. Now anyone can use your markdown dialect. Same for: syntax highlighters, BBCode parsers, wiki markup, LaTeX subsets, CSV-to-table converters, JSON-to-SX adapters. " (strong "The parser ecosystem becomes shared infrastructure."))
(~doc-code :code (highlight ";; A markdown parser, published to IPFS\n;; CID: bafy...md-parser\n(define parse-markdown\n (fn (source)\n ;; tokenize → build AST → return SX tree\n ;; (parse-markdown \"# Hello\\n**bold**\")\n ;; → (h1 \"Hello\") (p (strong \"bold\"))\n ...))\n\n;; Anyone can use it in their components\n(defcomp ~blog-post (&key markdown-source)\n (div :class \"prose\"\n (parse-markdown markdown-source)))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "Server-side and client-side logic")
(p "The same SX code runs on either side. A validation function published to IPFS runs server-side in Python for form processing and client-side in JavaScript for instant feedback. A price calculator runs server-side for order totals and client-side for live previews. " (em "The server/client split is a deployment decision, not a language boundary.")))
(div
(h4 :class "font-semibold text-stone-700" "Media and processing pipelines")
(p "Images, video, audio — all content-addressed on IPFS. But also the " (em "processing pipelines") " that created them. artdag DAGs are SX. Publish a DAG CID alongside the output CID and anyone can verify the provenance, re-render at different resolution, or fork the pipeline for their own work."))))
(~doc-subsection :title "The security model"
(p "This only works because of boundary enforcement. Every piece of SX fetched from the network runs within the receiver's security context:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Pure functions can't do IO. ") "A component from IPFS can produce markup — it cannot read your cookies, make network requests, access localStorage, or call any IO primitive. The boundary spec (boundary.sx) is enforced at registration time. This isn't a policy — it's structural. The evaluator literally doesn't have IO primitives available when running untrusted code.")
(li (strong "IO requires explicit grant. ") "Only locally-registered IO primitives (query, frag, current-user) have access to server resources. Fetched components never see them. The host decides what capabilities to grant.")
(li (strong "Step limits cap computation. ") "Untrusted code runs with configurable eval step limits. No infinite loops, no resource exhaustion. Exceeding the limit halts evaluation and returns an error node.")
(li (strong "Content addressing prevents tampering. ") "You asked for CID X, you got CID X, the hash proves it. No MITM, no CDN poisoning, no supply chain attacks on the content itself.")
(li (strong "Provenance proves authorship. ") "Bitcoin-anchored timestamps prove who published what and when. Not \"trust me\" — independently verifiable against the Bitcoin blockchain."))
(p "This is the opposite of the npm model. npm packages run with full access to your system — a malicious package can exfiltrate secrets, install backdoors, modify the filesystem. SX components are structurally sandboxed. The worst a malicious component can do is render a " (code "(div \"haha got you\")") "."))
(~doc-subsection :title "What this replaces"
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "Current web")
(th :class "px-3 py-2 font-medium text-stone-600" "SX web")
(th :class "px-3 py-2 font-medium text-stone-600" "Why")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "npm / package registries")
(td :class "px-3 py-2 text-stone-700" "IPFS + component CIDs")
(td :class "px-3 py-2 text-stone-600" "Content-addressed, no central authority, structurally safe"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "CDNs for JS/CSS")
(td :class "px-3 py-2 text-stone-700" "IPFS gateways")
(td :class "px-3 py-2 text-stone-600" "Permanent, decentralized, self-verifying"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "REST/GraphQL APIs")
(td :class "px-3 py-2 text-stone-700" "SX activities over AP")
(td :class "px-3 py-2 text-stone-600" "Responses are evaluable, not just data"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "HTML + CSS + JS")
(td :class "px-3 py-2 text-stone-700" "SX (one format)")
(td :class "px-3 py-2 text-stone-600" "No impedance mismatch, same evaluator everywhere"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Build tools (webpack, vite)")
(td :class "px-3 py-2 text-stone-700" "Nothing")
(td :class "px-3 py-2 text-stone-600" "SX evaluates directly, no compilation step"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Template languages")
(td :class "px-3 py-2 text-stone-700" "Nothing")
(td :class "px-3 py-2 text-stone-600" "SX is the template and the language"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "JSON-LD federation")
(td :class "px-3 py-2 text-stone-700" "SX federation")
(td :class "px-3 py-2 text-stone-600" "Wire format is executable, content renders itself"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Trust-based package security")
(td :class "px-3 py-2 text-stone-700" "Structural sandboxing")
(td :class "px-3 py-2 text-stone-600" "Pure functions can't have side effects — not by policy, by construction"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Servers + hosting + DNS + TLS")
(td :class "px-3 py-2 text-stone-700" "IPFS CID")
(td :class "px-3 py-2 text-stone-600" "Entire applications are content-addressed, no infrastructure needed"))))))
(~doc-subsection :title "Serverless applications on IPFS"
(p "The logical conclusion: " (strong "entire web applications hosted on IPFS with no server at all."))
(p "An SX application is a tree of content-addressed artifacts: a root page definition, component dependencies, media, stylesheets, parsers, transforms. Pin the root CID to IPFS and the application is live. No server, no DNS, no hosting provider, no deployment pipeline. Someone gives you a CID, you paste it into an SX-aware browser, and the application runs.")
(~doc-code :code (highlight ";; An entire blog — one CID\n;; ipfs://bafy...my-blog\n(defpage blog-home\n :path \"/\"\n :requires (list\n \"bafy...article-layout\" ;; layout component\n \"bafy...md-parser\" ;; markdown parser\n \"bafy...syntax-highlight\" ;; code highlighting\n \"bafy...nav-component\") ;; navigation\n :content\n (~article-layout\n :title \"My Blog\"\n :nav (~nav-component\n :items (list\n (dict :label \"Post 1\" :cid \"bafy...post-1\")\n (dict :label \"Post 2\" :cid \"bafy...post-2\")))\n :body (~markdown-page\n :source-cid \"bafy...homepage-md\")))" "lisp"))
(p "What this looks like in practice:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Personal sites: ") "A portfolio or blog is a handful of SX files + media. Pin to IPFS. Share the CID. No hosting costs, no domain renewal, no SSL certificates. The site is permanent.")
(li (strong "Documentation: ") "Pin your docs. They can't go offline, can't be censored, can't be altered after publication (provenance proves it). Anyone can mirror them by pinning the same CID.")
(li (strong "Collaborative applications: ") "Multiple authors contribute pages and components. Each publishes their CIDs. A root manifest composes them. Update the manifest CID to add content — old CIDs remain valid forever.")
(li (strong "Offline-first: ") "An IPFS-hosted app works the same whether you're online or have the content cached locally. The browser's SX evaluator + local IPFS node = complete offline platform.")
(li (strong "Zero-cost deployment: ") "\"Deploying\" means computing a hash. No CI/CD, no Docker, no cloud provider. Pin locally, pin to a remote IPFS node, or let others pin if they want to help host."))
(p "For applications that " (em "do") " need a server — user accounts, payments, real-time collaboration, database queries — the server provides IO primitives via the existing boundary system. The SX application fetches data from the server's IO endpoints, but the application itself (all the rendering, routing, component logic) lives on IPFS. The server is a " (em "data service") ", not an application host.")
(p "This inverts the current model. Today: server hosts the application, client is a thin renderer. SX web: IPFS hosts the application, server is an optional IO provider. " (strong "The application is the content. The content is the application. Both are just s-expressions.")))
(~doc-subsection :title "The end state"
(p "A browser with an SX evaluator and an IPFS gateway is a complete web platform. Given a CID — for a page, a post, an application — it can:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Fetch the content from IPFS")
(li "Resolve component dependencies (also from IPFS)")
(li "Resolve media (also from IPFS)")
(li "Evaluate the content (pure computation, sandboxed)")
(li "Render to DOM")
(li "Verify provenance (Bitcoin anchor)")
(li "Cache everything forever (content-addressed = immutable)"))
(p "No server needed. No DNS. No TLS certificates. No hosting provider. No build step. No framework. Just content-addressed s-expressions evaluating in a sandbox.")
(p "The server becomes optional infrastructure for " (em "IO") " — database queries, authentication, payment processing, real-time events. Everything else lives on the content-addressed network. The web stops being a collection of servers you visit and becomes a " (strong "shared evaluable space") " you participate in.")))
;; -----------------------------------------------------------------------
;; Cross-Cutting Concerns
;; -----------------------------------------------------------------------
(~doc-section :title "Cross-Cutting Concerns" :id "cross-cutting"
(~doc-subsection :title "Security"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Boundary enforcement is the foundation. ") "IPFS-fetched components are parsed and registered like any other component. SX_BOUNDARY_STRICT ensures they can't call IO primitives. A malicious component can produce ugly markup but can't exfiltrate data or make network requests.")
(li (strong "CID verification: ") "Content fetched from IPFS is hashed and compared to the expected CID before use. Tampered content is rejected.")
(li (strong "Signature chain: ") "Actor signatures (RSA/HTTP Signatures) prove authorship. Bitcoin anchors prove timing. Together they establish non-repudiable provenance.")
(li (strong "Resource limits: ") "Evaluation of untrusted components runs with step limits (max eval steps, max recursion depth). Infinite loops are caught and terminated.")))
(~doc-subsection :title "Backward Compatibility"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Content negotiation ensures legacy AP servers always receive valid JSON-LD")
(li "SX-Activity is strictly opt-in — servers that don't understand it get standard AP")
(li "Existing internal activity bus unchanged — SX format is for federation, not internal events")
(li "URL fallbacks on all media references — CID is preferred, URL is fallback")))
(~doc-subsection :title "Performance"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Component CIDs cached in localStorage forever (content-addressed = immutable)")
(li "IPFS gateway responses cached with long TTL (content can't change)")
(li "Local IPFS node (if present) eliminates gateway latency")
(li "Provenance verification is lazy — badge shows unverified until user clicks to verify")))
(~doc-subsection :title "Integration with Isomorphic Architecture"
(p "SX-Activity builds on the isomorphic architecture plan:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Phase 1 (component distribution) → IPFS replaces per-server bundles")
(li "Phase 2 (IO detection) → pure components safe for IPFS publication")
(li "Phase 3 (client routing) → client can resolve federated content without server")
(li "Phase 6 (streaming/suspense) → progressive IPFS resolution uses same infrastructure"))))
;; -----------------------------------------------------------------------
;; Critical Files
;; -----------------------------------------------------------------------
(~doc-section :title "Critical Files" :id "critical-files"
(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")
(th :class "px-3 py-2 font-medium text-stone-600" "Phases")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/infrastructure/activitypub.py")
(td :class "px-3 py-2 text-stone-700" "AP blueprint — add SX content negotiation")
(td :class "px-3 py-2 text-stone-600" "1"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/events/bus.py")
(td :class "px-3 py-2 text-stone-700" "Activity bus — add sx_source column, SX serialization")
(td :class "px-3 py-2 text-stone-600" "1"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/activity.py")
(td :class "px-3 py-2 text-stone-700" "SX ↔ JSON-LD bidirectional translation (new)")
(td :class "px-3 py-2 text-stone-600" "1"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ipfs.py")
(td :class "px-3 py-2 text-stone-700" "Component CID computation, IPFS pinning (new)")
(td :class "px-3 py-2 text-stone-600" "2, 3"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/ipfs-resolve.sx")
(td :class "px-3 py-2 text-stone-700" "Client-side IPFS resolution spec (new)")
(td :class "px-3 py-2 text-stone-600" "2, 3"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/models/federation.py")
(td :class "px-3 py-2 text-stone-700" "IPFSPin, APAnchor models — extend for components")
(td :class "px-3 py-2 text-stone-600" "2, 3, 5"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/registry.py")
(td :class "px-3 py-2 text-stone-700" "Component registry actor, discovery protocol (new)")
(td :class "px-3 py-2 text-stone-600" "4"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/anchor.py")
(td :class "px-3 py-2 text-stone-700" "Anchoring pipeline — wire to activity lifecycle (new)")
(td :class "px-3 py-2 text-stone-600" "5"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/boot.sx")
(td :class "px-3 py-2 text-stone-700" "Client boot — IPFS component loading")
(td :class "px-3 py-2 text-stone-600" "2, 6"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/events/handlers/ap_delivery_handler.py")
(td :class "px-3 py-2 text-stone-700" "Federation delivery — SX format support")
(td :class "px-3 py-2 text-stone-600" "1"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "artdag/core/artdag/cache.py")
(td :class "px-3 py-2 text-stone-700" "Content addressing — shared with component CIDs")
(td :class "px-3 py-2 text-stone-600" "2, 3"))))))))
;; ---------------------------------------------------------------------------
;; Content-Addressed Components
;; ---------------------------------------------------------------------------

133
sx/sx/plans/sx-ci.sx Normal file
View File

@@ -0,0 +1,133 @@
;; ---------------------------------------------------------------------------
;; SX CI Pipeline
;; ---------------------------------------------------------------------------
(defcomp ~plan-sx-ci-content ()
(~doc-page :title "SX CI Pipeline"
(p :class "text-stone-500 text-sm italic mb-8"
"Build, test, and deploy Rose Ash using the same language the application is written in.")
(~doc-section :title "Context" :id "context"
(p :class "text-stone-600"
"Rose Ash currently uses shell scripts for CI: " (code "deploy.sh") " auto-detects changed services via git diff, builds Docker images, pushes to the registry, and restarts Swarm services. " (code "dev.sh") " starts the dev environment and runs tests. These work, but they are opaque imperative scripts with no reuse, no composition, and no relationship to SX.")
(p :class "text-stone-600"
"The CI pipeline is the last piece of infrastructure not expressed in s-expressions. Fixing that completes the \"one representation for everything\" claim — the same language that defines the spec, the components, the pages, the essays, and the deployment config also defines the build pipeline."))
(~doc-section :title "Design" :id "design"
(p :class "text-stone-600"
"Pipeline definitions are " (code ".sx") " files. A minimal Python CLI runner evaluates them using " (code "sx_ref.py") ". CI-specific IO primitives (shell execution, Docker, git) are boundary-declared and only available to the pipeline runner — never to web components.")
(~doc-code :code (highlight ";; pipeline/deploy.sx\n(let ((targets (if (= (length ARGS) 0)\n (~detect-changed :base \"HEAD~1\")\n (filter (fn (svc) (some (fn (a) (= a (get svc \"name\"))) ARGS))\n services))))\n (when (= (length targets) 0)\n (log-step \"No changes detected\")\n (exit 0))\n\n (log-step (str \"Deploying: \" (join \" \" (map (fn (s) (get s \"name\")) targets))))\n\n ;; Tests first\n (~unit-tests)\n (~sx-spec-tests)\n\n ;; Build, push, restart\n (for-each (fn (svc) (~build-service :service svc)) targets)\n (for-each (fn (svc) (~restart-service :service svc)) targets)\n\n (log-step \"Deploy complete\"))" "lisp"))
(p :class "text-stone-600"
"Pipeline steps are components. " (code "~unit-tests") ", " (code "~build-service") ", " (code "~detect-changed") " are " (code "defcomp") " definitions that compose by nesting — the same mechanism used for page layouts, navigation, and every other piece of the system."))
(~doc-section :title "CI Primitives" :id "primitives"
(p :class "text-stone-600"
"New IO primitives declared in " (code "boundary.sx") ", implemented only in the CI runner context:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "Primitive")
(th :class "px-3 py-2 font-medium text-stone-600" "Signature")
(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 font-mono text-sm text-violet-700" "shell-run")
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(command) -> dict")
(td :class "px-3 py-2 text-stone-700" "Execute shell command, return " (code "{:exit N :stdout \"...\" :stderr \"...\"}") ""))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shell-run!")
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(command) -> dict")
(td :class "px-3 py-2 text-stone-700" "Execute shell command, throw on non-zero exit"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "docker-build")
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(&key file tag context) -> nil")
(td :class "px-3 py-2 text-stone-700" "Build Docker image"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "docker-push")
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(tag) -> nil")
(td :class "px-3 py-2 text-stone-700" "Push image to registry"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "docker-restart")
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(service) -> nil")
(td :class "px-3 py-2 text-stone-700" "Restart Swarm service"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "git-diff-files")
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(base head) -> list")
(td :class "px-3 py-2 text-stone-700" "List changed files between commits"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "git-branch")
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "() -> string")
(td :class "px-3 py-2 text-stone-700" "Current branch name"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "log-step")
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(message) -> nil")
(td :class "px-3 py-2 text-stone-700" "Formatted pipeline output"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "fail!")
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(message) -> nil")
(td :class "px-3 py-2 text-stone-700" "Abort pipeline with error")))))
(p :class "text-stone-600"
"The boundary system ensures these primitives are " (em "only") " available in the CI context. Web components cannot call " (code "shell-run!") " — the evaluator will refuse to resolve the symbol, just as it refuses to resolve any other unregistered IO primitive. The sandbox is structural, not a convention."))
(~doc-section :title "Reusable Steps" :id "steps"
(p :class "text-stone-600"
"Pipeline steps are components — same " (code "defcomp") " as UI components, same " (code "&key") " params, same composition by nesting:")
(~doc-code :code (highlight "(defcomp ~detect-changed (&key base)\n (let ((files (git-diff-files (or base \"HEAD~1\") \"HEAD\")))\n (if (some (fn (f) (starts-with? f \"shared/\")) files)\n services\n (filter (fn (svc)\n (some (fn (f) (starts-with? f (str (get svc \"dir\") \"/\"))) files))\n services))))\n\n(defcomp ~build-service (&key service)\n (let ((name (get service \"name\"))\n (tag (str registry \"/\" name \":latest\")))\n (log-step (str \"Building \" name))\n (docker-build :file (str (get service \"dir\") \"/Dockerfile\") :tag tag :context \".\")\n (docker-push tag)))\n\n(defcomp ~bootstrap-check ()\n (log-step \"Checking bootstrapped files are up to date\")\n (shell-run! \"python shared/sx/ref/bootstrap_js.py\")\n (shell-run! \"python shared/sx/ref/bootstrap_py.py\")\n (let ((diff (shell-run \"git diff --name-only shared/static/scripts/sx-ref.js shared/sx/ref/sx_ref.py\")))\n (when (not (= (get diff \"stdout\") \"\"))\n (fail! \"Bootstrapped files are stale — rebootstrap and commit\"))))" "lisp"))
(p :class "text-stone-600"
"Compare this to GitHub Actions YAML, where \"reuse\" means composite actions with " (code "uses:") " references, input/output mappings, shell script blocks inside YAML strings, and a " (a :href "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions" :class "text-violet-600 hover:underline" "100-page syntax reference") ". SX pipeline reuse is function composition. That is all it has ever been."))
(~doc-section :title "Pipelines" :id "pipelines"
(p :class "text-stone-600"
"Two primary pipelines, each a single " (code ".sx") " file:")
(div :class "space-y-4"
(div :class "rounded border border-stone-200 p-4"
(h4 :class "font-semibold text-stone-700 mb-2" "pipeline/test.sx")
(p :class "text-sm text-stone-600" "Unit tests, SX spec tests (Python + Node), bootstrap staleness check, Tailwind CSS check. Run locally or in CI.")
(p :class "text-sm font-mono text-violet-700 mt-1" "python -m shared.sx.ci pipeline/test.sx"))
(div :class "rounded border border-stone-200 p-4"
(h4 :class "font-semibold text-stone-700 mb-2" "pipeline/deploy.sx")
(p :class "text-sm text-stone-600" "Auto-detect changed services (or accept explicit args), run tests, build Docker images, push to registry, restart Swarm services.")
(p :class "text-sm font-mono text-violet-700 mt-1" "python -m shared.sx.ci pipeline/deploy.sx blog market"))))
(~doc-section :title "Why this matters" :id "why"
(p :class "text-stone-600"
"CI pipelines are the strongest test case for \"one representation for everything.\" GitHub Actions, GitLab CI, CircleCI — all use YAML. YAML is not a programming language. So every CI system reinvents conditionals (" (code "if:") " expressions evaluated as strings), iteration (" (code "matrix:") " strategies), composition (" (code "uses:") " references with input/output schemas), and error handling (" (code "continue-on-error:") " booleans) — all in a data format that was never designed for any of it.")
(p :class "text-stone-600"
"The result is a domain-specific language trapped inside YAML, with worse syntax than any language designed to be one. Every CI pipeline of sufficient complexity becomes a programming task performed in a notation that actively resists programming.")
(p :class "text-stone-600"
"SX pipelines use real conditionals, real functions, real composition, and real error handling — because SX is a real language. The pipeline definition and the application code are the same thing. An AI that can generate SX components can generate SX pipelines. A developer who reads SX pages can read SX deploys. The representation is universal."))
(~doc-section :title "Files" :id "files"
(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" "Purpose")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ci.py")
(td :class "px-3 py-2 text-stone-700" "Pipeline runner CLI (~150 lines)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ci_primitives.py")
(td :class "px-3 py-2 text-stone-700" "CI IO primitive implementations"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "pipeline/services.sx")
(td :class "px-3 py-2 text-stone-700" "Service registry (data)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "pipeline/steps.sx")
(td :class "px-3 py-2 text-stone-700" "Reusable step components"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "pipeline/test.sx")
(td :class "px-3 py-2 text-stone-700" "Test pipeline"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "pipeline/deploy.sx")
(td :class "px-3 py-2 text-stone-700" "Deploy pipeline"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/boundary.sx")
(td :class "px-3 py-2 text-stone-700" "Add CI primitive declarations"))))))))
;; ---------------------------------------------------------------------------
;; Live Streaming — SSE & WebSocket
;; ---------------------------------------------------------------------------

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,186 @@
;; ---------------------------------------------------------------------------
;; Demo page — shows what's been implemented
;; ---------------------------------------------------------------------------
(defcomp ~reactive-islands-demo-content ()
(~doc-page :title "Reactive Islands Demo"
(~doc-section :title "What this demonstrates" :id "what"
(p (strong "These are live interactive islands") " — not static code snippets. Click the buttons. The signal runtime is defined in " (code "signals.sx") " (374 lines of s-expressions), then bootstrapped to JavaScript by " (code "bootstrap_js.py") ". No hand-written signal logic in JavaScript.")
(p "The transpiled " (code "sx-browser.js") " registers " (code "signal") ", " (code "deref") ", " (code "reset!") ", " (code "swap!") ", " (code "computed") ", " (code "effect") ", and " (code "batch") " as SX primitives — callable from " (code "defisland") " bodies defined in " (code ".sx") " files."))
(~doc-section :title "1. Signal + Computed + Effect" :id "demo-counter"
(p "A signal holds a value. A computed derives from it. Click the buttons — the counter and doubled value update instantly, no server round-trip.")
(~demo-counter :initial 0)
(~doc-code :code (highlight "(defisland ~demo-counter (&key initial)\n (let ((count (signal (or initial 0)))\n (doubled (computed (fn () (* 2 (deref count))))))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! count dec)) \"\")\n (span (deref count))\n (button :on-click (fn (e) (swap! count inc)) \"+\")\n (p \"doubled: \" (deref doubled)))))" "lisp"))
(p (code "(deref count)") " in a text position creates a reactive text node. When " (code "count") " changes, " (em "only that text node") " updates. " (code "doubled") " recomputes automatically. No diffing."))
(~doc-section :title "2. Temperature Converter" :id "demo-temperature"
(p "Two derived values from one signal. Click to change Celsius — Fahrenheit updates reactively.")
(~demo-temperature)
(~doc-code :code (highlight "(defisland ~demo-temperature ()\n (let ((celsius (signal 20)))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! celsius (fn (c) (- c 5)))) \"5\")\n (span (deref celsius))\n (button :on-click (fn (e) (swap! celsius (fn (c) (+ c 5)))) \"+5\")\n (span \"°C = \")\n (span (+ (* (deref celsius) 1.8) 32))\n (span \"°F\"))))" "lisp"))
(p "The actual implementation uses " (code "computed") " for Fahrenheit: " (code "(computed (fn () (+ (* (deref celsius) 1.8) 32)))") ". The " (code "(deref fahrenheit)") " in the span creates a reactive text node that updates when celsius changes."))
(~doc-section :title "3. Effect + Cleanup: Stopwatch" :id "demo-stopwatch"
(p "Effects can return cleanup functions. This stopwatch starts a " (code "set-interval") " — the cleanup clears it when the running signal toggles off.")
(~demo-stopwatch)
(~doc-code :code (highlight "(defisland ~demo-stopwatch ()\n (let ((running (signal false))\n (elapsed (signal 0))\n (time-text (create-text-node \"0.0s\"))\n (btn-text (create-text-node \"Start\")))\n ;; Timer: effect creates interval, cleanup clears it\n (effect (fn ()\n (when (deref running)\n (let ((id (set-interval (fn () (swap! elapsed inc)) 100)))\n (fn () (clear-interval id))))))\n ;; Display: updates text node when elapsed changes\n (effect (fn ()\n (let ((e (deref elapsed)))\n (dom-set-text-content time-text\n (str (floor (/ e 10)) \".\" (mod e 10) \"s\")))))\n ;; Button label\n (effect (fn ()\n (dom-set-text-content btn-text\n (if (deref running) \"Stop\" \"Start\"))))\n (div :class \"...\"\n (span time-text)\n (button :on-click (fn (e) (swap! running not)) btn-text)\n (button :on-click (fn (e)\n (reset! running false) (reset! elapsed 0)) \"Reset\"))))" "lisp"))
(p "Three effects, each tracking different signals. The timer effect's cleanup fires before each re-run — toggling " (code "running") " off clears the interval. No hook rules: effects can appear anywhere, in any order."))
(~doc-section :title "4. Imperative Pattern" :id "demo-imperative"
(p "For complex reactivity (dynamic classes, conditional text), use the imperative pattern: " (code "create-text-node") " + " (code "effect") " + " (code "dom-set-text-content") ".")
(~demo-imperative)
(~doc-code :code (highlight "(defisland ~demo-imperative ()\n (let ((count (signal 0))\n (text-node (create-text-node \"0\")))\n ;; Explicit effect: re-runs when count changes\n (effect (fn ()\n (dom-set-text-content text-node (str (deref count)))))\n (div :class \"...\"\n (span text-node)\n (button :on-click (fn (e) (swap! count inc)) \"+\"))))" "lisp"))
(p "Two patterns exist: " (strong "declarative") " (" (code "(span (deref sig))") " — auto-reactive via " (code "reactive-text") ") and " (strong "imperative") " (" (code "create-text-node") " + " (code "effect") " — explicit, full control). Use declarative for simple text, imperative for dynamic classes, conditional DOM, or complex updates."))
(~doc-section :title "5. Reactive List" :id "demo-reactive-list"
(p "When " (code "map") " is used with " (code "(deref signal)") " inside an island, it auto-upgrades to a reactive list. With " (code ":key") " attributes, existing DOM nodes are reused across updates — only additions, removals, and reorderings touch the DOM.")
(~demo-reactive-list)
(~doc-code :code (highlight "(defisland ~demo-reactive-list ()\n (let ((next-id (signal 1))\n (items (signal (list)))\n (add-item (fn (e)\n (batch (fn ()\n (swap! items (fn (old)\n (append old (dict \"id\" (deref next-id)\n \"text\" (str \"Item \" (deref next-id))))))\n (swap! next-id inc)))))\n (remove-item (fn (id)\n (swap! items (fn (old)\n (filter (fn (item) (not (= (get item \"id\") id))) old))))))\n (div\n (button :on-click add-item \"Add Item\")\n (span (deref (computed (fn () (len (deref items))))) \" items\")\n (ul\n (map (fn (item)\n (li :key (str (get item \"id\"))\n (span (get item \"text\"))\n (button :on-click (fn (e) (remove-item (get item \"id\"))) \"✕\")))\n (deref items))))))" "lisp"))
(p (code ":key") " identifies each list item. When items change, the reconciler matches old and new keys — reusing existing DOM nodes, inserting new ones, and removing stale ones. Without keys, the list falls back to clear-and-rerender. " (code "batch") " groups the two signal writes into one update pass."))
(~doc-section :title "6. Input Binding" :id "demo-input-binding"
(p "The " (code ":bind") " attribute creates a two-way link between a signal and a form element. Type in the input — the signal updates. Change the signal — the input updates. Works with text inputs, checkboxes, radios, textareas, and selects.")
(~demo-input-binding)
(~doc-code :code (highlight "(defisland ~demo-input-binding ()\n (let ((name (signal \"\"))\n (agreed (signal false)))\n (div\n (input :type \"text\" :bind name\n :placeholder \"Type your name...\")\n (span \"Hello, \" (strong (deref name)) \"!\")\n (input :type \"checkbox\" :bind agreed)\n (when (deref agreed)\n (p \"Thanks for agreeing!\")))))" "lisp"))
(p (code ":bind") " detects the element type automatically — text inputs use " (code "value") " + " (code "input") " event, checkboxes use " (code "checked") " + " (code "change") " event. The effect only updates the DOM when the value actually changed, preventing cursor jump."))
(~doc-section :title "7. Portals" :id "demo-portal"
(p "A " (code "portal") " renders children into a DOM node " (em "outside") " the island's subtree. Essential for modals, tooltips, and toasts — anything that must escape " (code "overflow:hidden") " or z-index stacking.")
(~demo-portal)
(~doc-code :code (highlight "(defisland ~demo-portal ()\n (let ((open? (signal false)))\n (div\n (button :on-click (fn (e) (swap! open? not))\n (if (deref open?) \"Close Modal\" \"Open Modal\"))\n (portal \"#portal-root\"\n (when (deref open?)\n (div :class \"fixed inset-0 bg-black/50 ...\"\n :on-click (fn (e) (reset! open? false))\n (div :class \"bg-white rounded-lg p-6 ...\"\n :on-click (fn (e) (stop-propagation e))\n (h2 \"Portal Modal\")\n (p \"Rendered outside the island's DOM.\")\n (button :on-click (fn (e) (reset! open? false))\n \"Close\"))))))))" "lisp"))
(p "The portal content lives in " (code "#portal-root") " (typically at the page body level), not inside the island. On island disposal, portal content is automatically removed from its target — the " (code "register-in-scope") " mechanism handles cleanup."))
(~doc-section :title "8. Error Boundaries" :id "demo-error-boundary"
(p "When an island's rendering or effect throws, " (code "error-boundary") " catches the error and renders a fallback. The fallback receives the error and a retry function. Partial effects created before the error are disposed automatically.")
(~demo-error-boundary)
(~doc-code :code (highlight "(defisland ~demo-error-boundary ()\n (let ((throw? (signal false)))\n (error-boundary\n ;; Fallback: receives (err retry-fn)\n (fn (err retry-fn)\n (div :class \"p-3 bg-red-50 border border-red-200 rounded\"\n (p :class \"text-red-700\" (error-message err))\n (button :on-click (fn (e)\n (reset! throw? false) (invoke retry-fn))\n \"Retry\")))\n ;; Children: the happy path\n (do\n (when (deref throw?) (error \"Intentional explosion!\"))\n (p \"Everything is fine.\")))))" "lisp"))
(p "React equivalent: " (code "componentDidCatch") " / " (code "ErrorBoundary") ". SX's version is simpler — one form, not a class. The " (code "error-boundary") " form is a render-dom special form in " (code "adapter-dom.sx") "."))
(~doc-section :title "9. Refs — Imperative DOM Access" :id "demo-refs"
(p "The " (code ":ref") " attribute captures a DOM element handle into a dict. Use it for imperative operations: focusing, measuring, reading values.")
(~demo-refs)
(~doc-code :code (highlight "(defisland ~demo-refs ()\n (let ((my-ref (dict \"current\" nil))\n (msg (signal \"\")))\n (input :ref my-ref :type \"text\"\n :placeholder \"I can be focused programmatically\")\n (button :on-click (fn (e)\n (dom-focus (get my-ref \"current\")))\n \"Focus Input\")\n (button :on-click (fn (e)\n (let ((el (get my-ref \"current\")))\n (reset! msg (str \"value: \" (dom-get-prop el \"value\")))))\n \"Read Input\")\n (when (not (= (deref msg) \"\"))\n (p (deref msg)))))" "lisp"))
(p "React equivalent: " (code "useRef") ". In SX, a ref is just " (code "(dict \"current\" nil)") " — no special API. The " (code ":ref") " attribute sets " (code "(dict-set! ref \"current\" el)") " when the element is created. Read it with " (code "(get ref \"current\")") "."))
(~doc-section :title "10. Dynamic Class and Style" :id "demo-dynamic-class"
(p "React uses " (code "className") " and " (code "style") " props with state. SX does the same — " (code "(deref signal)") " inside a " (code ":class") " or " (code ":style") " attribute creates a reactive binding. The attribute updates when the signal changes.")
(~demo-dynamic-class)
(~doc-code :code (highlight "(defisland ~demo-dynamic-class ()\n (let ((danger (signal false))\n (size (signal 16)))\n (div\n (button :on-click (fn (e) (swap! danger not))\n (if (deref danger) \"Safe mode\" \"Danger mode\"))\n (button :on-click (fn (e) (swap! size (fn (s) (+ s 2))))\n \"Bigger\")\n ;; Reactive class — recomputed when danger changes\n (div :class (str \"p-3 rounded font-medium \"\n (if (deref danger)\n \"bg-red-100 text-red-800\"\n \"bg-green-100 text-green-800\"))\n ;; Reactive style — recomputed when size changes\n :style (str \"font-size:\" (deref size) \"px\")\n \"This element's class and style are reactive.\"))))" "lisp"))
(p "React equivalent: " (code "className={danger ? 'red' : 'green'}") " and " (code "style={{fontSize: size}}") ". In SX the " (code "str") " + " (code "if") " + " (code "deref") " pattern handles it — no " (code "classnames") " library needed. For complex conditional classes, use a " (code "computed") " or a CSSX " (code "defcomp") " that returns a class string."))
(~doc-section :title "11. Resource + Suspense Pattern" :id "demo-resource"
(p (code "resource") " wraps an async operation into a signal with " (code "loading") "/" (code "data") "/" (code "error") " states. Combined with " (code "cond") " + " (code "deref") ", this is the suspense pattern — no special form needed.")
(~demo-resource)
(~doc-code :code (highlight "(defisland ~demo-resource ()\n (let ((data (resource (fn ()\n ;; Any promise-returning function\n (promise-delayed 1500\n (dict \"name\" \"Ada Lovelace\"\n \"role\" \"First Programmer\"))))))\n ;; This IS the suspense pattern:\n (let ((state (deref data)))\n (cond\n (get state \"loading\")\n (div \"Loading...\")\n (get state \"error\")\n (div \"Error: \" (get state \"error\"))\n :else\n (div (get (get state \"data\") \"name\"))))))" "lisp"))
(p "React equivalent: " (code "Suspense") " + " (code "use()") " or " (code "useSWR") ". SX doesn't need a special " (code "suspense") " form because " (code "resource") " returns a signal and " (code "cond") " + " (code "deref") " creates reactive conditional rendering. When the promise resolves, the signal updates and the " (code "cond") " branch switches automatically."))
(~doc-section :title "12. Transition Pattern" :id "demo-transition"
(p "React's " (code "startTransition") " defers non-urgent updates so typing stays responsive. In SX: " (code "schedule-idle") " + " (code "batch") ". The filter runs during idle time, not blocking the input event.")
(~demo-transition)
(~doc-code :code (highlight "(defisland ~demo-transition ()\n (let ((query (signal \"\"))\n (all-items (list \"Signals\" \"Effects\" ...))\n (filtered (signal (list)))\n (pending (signal false)))\n (reset! filtered all-items)\n ;; Filter effect — deferred via schedule-idle\n (effect (fn ()\n (let ((q (lower (deref query))))\n (if (= q \"\")\n (do (reset! pending false)\n (reset! filtered all-items))\n (do (reset! pending true)\n (schedule-idle (fn ()\n (batch (fn ()\n (reset! filtered\n (filter (fn (item)\n (contains? (lower item) q))\n all-items))\n (reset! pending false))))))))))\n (div\n (input :bind query :placeholder \"Filter...\")\n (when (deref pending) (span \"Filtering...\"))\n (ul (map (fn (item) (li :key item item))\n (deref filtered))))))" "lisp"))
(p "React equivalent: " (code "startTransition(() => setFiltered(...))") ". SX uses " (code "schedule-idle") " (" (code "requestIdleCallback") " under the hood) to defer the expensive " (code "filter") " operation, and " (code "batch") " to group the result into one update. Fine-grained signals already avoid the jank that makes transitions critical in React — this pattern is for truly expensive computations."))
(~doc-section :title "13. Shared Stores" :id "demo-stores"
(p "React uses " (code "Context") " or state management libraries for cross-component state. SX uses " (code "def-store") " / " (code "use-store") " — named signal containers that persist across island creation/destruction.")
(~demo-store-writer)
(~demo-store-reader)
(~doc-code :code (highlight ";; Island A — creates/writes the store\n(defisland ~store-writer ()\n (let ((store (def-store \"theme\" (fn ()\n (dict \"color\" (signal \"violet\")\n \"dark\" (signal false))))))\n (select :bind (get store \"color\")\n (option :value \"violet\" \"Violet\")\n (option :value \"blue\" \"Blue\"))\n (input :type \"checkbox\" :bind (get store \"dark\"))))\n\n;; Island B — reads the same store, different island\n(defisland ~store-reader ()\n (let ((store (use-store \"theme\")))\n (div :class (str \"bg-\" (deref (get store \"color\")) \"-100\")\n \"Styled by signals from Island A\")))" "lisp"))
(p "React equivalent: " (code "createContext") " + " (code "useContext") " or Redux/Zustand. Stores are simpler — just named dicts of signals at page scope. " (code "def-store") " creates once, " (code "use-store") " retrieves. Stores survive island disposal but clear on full page navigation."))
(~doc-section :title "14. How defisland Works" :id "how-defisland"
(p (code "defisland") " creates a reactive component. Same calling convention as " (code "defcomp") " — keyword args, rest children — but with a reactive boundary. Inside an island, " (code "deref") " subscribes DOM nodes to signals.")
(~doc-code :code (highlight ";; Definition — same syntax as defcomp\n(defisland ~counter (&key initial)\n (let ((count (signal (or initial 0))))\n (div\n (span (deref count)) ;; reactive text node\n (button :on-click (fn (e) (swap! count inc)) ;; event handler\n \"+\"))))\n\n;; Usage — same as any component\n(~counter :initial 42)\n\n;; Server-side rendering:\n;; <div data-sx-island=\"counter\" data-sx-state='{\"initial\":42}'>\n;; <span>42</span><button>+</button>\n;; </div>\n;;\n;; Client hydrates: signals + effects + event handlers attach" "lisp"))
(p "Each " (code "deref") " call registers the enclosing DOM node as a subscriber. Signal changes update " (em "only") " the subscribed nodes — no virtual DOM, no diffing, no component re-renders."))
(~doc-section :title "15. Test suite" :id "demo-tests"
(p "17 tests verify the signal runtime against the spec. All pass in the Python test runner (which uses the hand-written evaluator with native platform primitives).")
(~doc-code :code (highlight ";; Signal basics (6 tests)\n(assert-true (signal? (signal 42)))\n(assert-equal 42 (deref (signal 42)))\n(assert-equal 5 (deref 5)) ;; non-signal passthrough\n\n;; reset! changes value\n(let ((s (signal 0)))\n (reset! s 10)\n (assert-equal 10 (deref s)))\n\n;; reset! does NOT notify when value unchanged (identical? check)\n\n;; Computed (3 tests)\n(let ((a (signal 3)) (b (signal 4))\n (sum (computed (fn () (+ (deref a) (deref b))))))\n (assert-equal 7 (deref sum))\n (reset! a 10)\n (assert-equal 14 (deref sum)))\n\n;; Effects (4 tests) — immediate run, re-run on change, dispose, cleanup\n;; Batch (1 test) — defers notifications, deduplicates subscribers\n;; defisland (3 tests) — creates island, callable, accepts children" "lisp"))
(p :class "mt-2 text-sm text-stone-500" "Run: " (code "python3 shared/sx/tests/run.py signals")))
(~doc-section :title "React Feature Coverage" :id "coverage"
(p "Every React feature has an SX equivalent — most are simpler because signals are fine-grained.")
(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" "React")
(th :class "px-3 py-2 font-medium text-stone-600" "SX")
(th :class "px-3 py-2 font-medium text-stone-600" "Demo")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "useState")
(td :class "px-3 py-2 font-mono text-xs text-violet-700" "(signal value)")
(td :class "px-3 py-2 text-xs text-stone-500" "#1"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "useMemo")
(td :class "px-3 py-2 font-mono text-xs text-violet-700" "(computed (fn () ...))")
(td :class "px-3 py-2 text-xs text-stone-500" "#1, #2"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "useEffect")
(td :class "px-3 py-2 font-mono text-xs text-violet-700" "(effect (fn () ...))")
(td :class "px-3 py-2 text-xs text-stone-500" "#3"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "useRef")
(td :class "px-3 py-2 font-mono text-xs text-violet-700" "(dict \"current\" nil) + :ref")
(td :class "px-3 py-2 text-xs text-stone-500" "#9"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "useCallback")
(td :class "px-3 py-2 font-mono text-xs text-violet-700" "(fn (...) ...) — no dep arrays")
(td :class "px-3 py-2 text-xs text-stone-500" "N/A"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "className / style")
(td :class "px-3 py-2 font-mono text-xs text-violet-700" ":class (str ...) :style (str ...)")
(td :class "px-3 py-2 text-xs text-stone-500" "#10"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Controlled inputs")
(td :class "px-3 py-2 font-mono text-xs text-violet-700" ":bind signal")
(td :class "px-3 py-2 text-xs text-stone-500" "#6"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "key prop")
(td :class "px-3 py-2 font-mono text-xs text-violet-700" ":key value")
(td :class "px-3 py-2 text-xs text-stone-500" "#5"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "createPortal")
(td :class "px-3 py-2 font-mono text-xs text-violet-700" "(portal \"#target\" ...)")
(td :class "px-3 py-2 text-xs text-stone-500" "#7"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "ErrorBoundary")
(td :class "px-3 py-2 font-mono text-xs text-violet-700" "(error-boundary fallback ...)")
(td :class "px-3 py-2 text-xs text-stone-500" "#8"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Suspense + use()")
(td :class "px-3 py-2 font-mono text-xs text-violet-700" "(resource fn) + cond/deref")
(td :class "px-3 py-2 text-xs text-stone-500" "#11"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "startTransition")
(td :class "px-3 py-2 font-mono text-xs text-violet-700" "schedule-idle + batch")
(td :class "px-3 py-2 text-xs text-stone-500" "#12"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Context / Redux")
(td :class "px-3 py-2 font-mono text-xs text-violet-700" "def-store / use-store")
(td :class "px-3 py-2 text-xs text-stone-500" "#13"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Virtual DOM / diffing")
(td :class "px-3 py-2 text-xs text-stone-500" "N/A — fine-grained signals update exact DOM nodes")
(td :class "px-3 py-2 text-xs text-stone-500" ""))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "JSX / build step")
(td :class "px-3 py-2 text-xs text-stone-500" "N/A — s-expressions are the syntax")
(td :class "px-3 py-2 text-xs text-stone-500" ""))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Server Components")
(td :class "px-3 py-2 text-xs text-stone-500" "N/A — aser mode already expands server-side")
(td :class "px-3 py-2 text-xs text-stone-500" ""))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Concurrent rendering")
(td :class "px-3 py-2 text-xs text-stone-500" "N/A — fine-grained updates are inherently incremental")
(td :class "px-3 py-2 text-xs text-stone-500" ""))
(tr
(td :class "px-3 py-2 text-stone-700" "Hooks rules")
(td :class "px-3 py-2 text-xs text-stone-500" "N/A — signals are values, no ordering rules")
(td :class "px-3 py-2 text-xs text-stone-500" ""))))))))
;; ---------------------------------------------------------------------------
;; Event Bridge — DOM events for lake→island communication
;; ---------------------------------------------------------------------------

View File

@@ -0,0 +1,58 @@
;; ---------------------------------------------------------------------------
;; Event Bridge — DOM events for lake→island communication
;; ---------------------------------------------------------------------------
(defcomp ~reactive-islands-event-bridge-content ()
(~doc-page :title "Event Bridge"
(~doc-section :title "The Problem" :id "problem"
(p "A reactive island can contain server-rendered content — an htmx \"lake\" that swaps via " (code "sx-get") "/" (code "sx-post") ". The lake content is pure HTML from the server. It has no access to island signals.")
(p "But sometimes the lake needs to " (em "tell") " the island something happened. A server-rendered \"Add to Cart\" button needs to update the island's cart signal. A server-rendered search form needs to feed results into the island's result signal.")
(p "The event bridge solves this: DOM custom events bubble from the lake up to the island, where an effect listens and updates signals."))
(~doc-section :title "How it works" :id "how"
(p "Three components:")
(ol :class "space-y-2 text-stone-600 list-decimal list-inside"
(li (strong "Server emits: ") "Server-rendered elements carry " (code "data-sx-emit") " attributes. When the user interacts, the client dispatches a CustomEvent.")
(li (strong "Event bubbles: ") "The event bubbles up through the DOM tree until it reaches the island container.")
(li (strong "Effect catches: ") "An effect inside the island listens for the event name and updates a signal."))
(~doc-code :code (highlight ";; Island with an event bridge\n(defisland ~product-page (&key product)\n (let ((cart-items (signal (list))))\n\n ;; Bridge: listen for \"cart:add\" events from server content\n (bridge-event container \"cart:add\" cart-items\n (fn (detail)\n (append (deref cart-items)\n (dict :id (get detail \"id\")\n :name (get detail \"name\")\n :price (get detail \"price\")))))\n\n (div\n ;; Island header with reactive cart count\n (div :class \"flex justify-between\"\n (h1 (get product \"name\"))\n (span :class \"badge\" (length (deref cart-items)) \" items\"))\n\n ;; htmx lake — server-rendered product details\n ;; This content is swapped by sx-get, not rendered by the island\n (div :id \"product-details\"\n :sx-get (str \"/products/\" (get product \"id\") \"/details\")\n :sx-swap \"innerHTML\"\n :sx-trigger \"load\"))))" "lisp"))
(p "The server handler for " (code "/products/:id/details") " returns HTML with emit attributes:")
(~doc-code :code (highlight ";; Server-rendered response (pure HTML, no signals)\n(div\n (p (get product \"description\"))\n (div :class \"flex gap-2 mt-4\"\n (button\n :data-sx-emit \"cart:add\"\n :data-sx-emit-detail (json-serialize\n (dict :id (get product \"id\")\n :name (get product \"name\")\n :price (get product \"price\")))\n :class \"bg-violet-600 text-white px-4 py-2 rounded\"\n \"Add to Cart\")))" "lisp"))
(p "The button is plain server HTML. When clicked, the client's event bridge dispatches " (code "cart:add") " with the JSON detail. The island effect catches it and appends to " (code "cart-items") ". The badge updates reactively."))
(~doc-section :title "Why signals survive swaps" :id "survival"
(p "Signals live in JavaScript memory (closures), not in the DOM. When htmx swaps content inside an island:")
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
(li (strong "Swap inside island: ") "Signals survive. The lake content is replaced but the island's signal closures are untouched. Effects re-bind to new DOM nodes if needed.")
(li (strong "Swap outside island: ") "Signals survive. The island is not affected by swaps to other parts of the page.")
(li (strong "Swap replaces island: ") "Signals are " (em "lost") ". The island is disposed. This is where " (a :href "/reactive-islands/named-stores" :sx-get "/reactive-islands/named-stores" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "named stores") " come in — they persist at page level, surviving island destruction.")))
(~doc-section :title "Spec" :id "spec"
(p "The event bridge is spec'd in " (code "signals.sx") " (sections 12-13). Three functions:")
(~doc-code :code (highlight ";; Low-level: dispatch a custom event\n(emit-event el \"cart:add\" {:id 42 :name \"Widget\"})\n\n;; Low-level: listen for a custom event\n(on-event container \"cart:add\" (fn (e)\n (swap! items (fn (old) (append old (event-detail e))))))\n\n;; High-level: bridge an event directly to a signal\n;; Creates an effect with automatic cleanup on dispose\n(bridge-event container \"cart:add\" items\n (fn (detail) (append (deref items) detail)))" "lisp"))
(p "Platform interface required:")
(div :class "overflow-x-auto rounded border border-stone-200 mt-2"
(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" "Function")
(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 font-mono text-sm text-violet-700" "(dom-listen el name handler)")
(td :class "px-3 py-2 text-stone-700" "Attach event listener, return remove function"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "(dom-dispatch el name detail)")
(td :class "px-3 py-2 text-stone-700" "Dispatch CustomEvent with detail, bubbles: true"))
(tr
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "(event-detail e)")
(td :class "px-3 py-2 text-stone-700" "Extract .detail from CustomEvent"))))))))
;; ---------------------------------------------------------------------------
;; Named Stores — page-level signal containers
;; ---------------------------------------------------------------------------

View File

@@ -0,0 +1,488 @@
;; Reactive Islands section — top-level section for the reactive islands system.
;; ---------------------------------------------------------------------------
;; Index / Overview
;; ---------------------------------------------------------------------------
(defcomp ~reactive-islands-index-content ()
(~doc-page :title "Reactive Islands"
(~doc-section :title "Architecture" :id "architecture"
(p "Two orthogonal bars control how an SX page works:")
(ul :class "space-y-1 text-stone-600 list-disc pl-5"
(li (strong "Render boundary") " — where rendering happens (server HTML vs client DOM)")
(li (strong "State flow") " — how state flows (server state vs client signals)"))
(div :class "overflow-x-auto mt-4 mb-4"
(table :class "w-full text-sm text-left"
(thead
(tr :class "border-b border-stone-200"
(th :class "py-2 px-3 font-semibold text-stone-700" "")
(th :class "py-2 px-3 font-semibold text-stone-700" "Server State")
(th :class "py-2 px-3 font-semibold text-stone-700" "Client State")))
(tbody :class "text-stone-600"
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-semibold text-stone-700" "Server Rendering")
(td :class "py-2 px-3" "Pure hypermedia (htmx)")
(td :class "py-2 px-3" "SSR + hydrated islands"))
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-semibold text-stone-700" "Client Rendering")
(td :class "py-2 px-3" "SX wire format (current)")
(td :class "py-2 px-3 font-semibold text-violet-700" "Reactive islands (this)")))))
(p "Most content stays pure hypermedia. Interactive regions opt into reactivity. The author controls where each component sits on both bars."))
(~doc-section :title "Four Levels" :id "levels"
(div :class "space-y-4"
(div :class "rounded border border-stone-200 p-4"
(div :class "font-semibold text-stone-800" "Level 0: Pure Hypermedia")
(p :class "text-sm text-stone-600 mt-1"
"The default. " (code "sx-get") ", " (code "sx-post") ", " (code "sx-swap") ". Server renders everything. No client state. 90% of a typical application."))
(div :class "rounded border border-stone-200 p-4"
(div :class "font-semibold text-stone-800" "Level 1: Local DOM Operations")
(p :class "text-sm text-stone-600 mt-1"
"Imperative escapes: " (code "toggle!") ", " (code "set-attr!") ", " (code "on-event") ". Micro-interactions too small for a server round-trip."))
(div :class "rounded border border-violet-300 bg-violet-50 p-4"
(div :class "font-semibold text-violet-900" "Level 2: Reactive Islands")
(p :class "text-sm text-stone-600 mt-1"
(code "defisland") " components with local signals. Fine-grained DOM updates " (em "without") " virtual DOM, diffing, or component re-renders. A signal change updates only the DOM nodes that read it."))
(div :class "rounded border border-stone-200 p-4"
(div :class "font-semibold text-stone-800" "Level 3: Connected Islands")
(p :class "text-sm text-stone-600 mt-1"
"Islands that share state via signal props or named stores (" (code "def-store") " / " (code "use-store") ")."))))
(~doc-section :title "Signal Primitives" :id "signals"
(~doc-code :code (highlight "(signal v) ;; create a reactive container\n(deref s) ;; read value — subscribes in reactive context\n(reset! s v) ;; write new value — notifies subscribers\n(swap! s f) ;; update via function: (f old-value)\n(computed fn) ;; derived signal — auto-tracks dependencies\n(effect fn) ;; side effect — re-runs when deps change\n(batch fn) ;; group writes — one notification pass" "lisp"))
(p "Signals are values, not hooks. Create them anywhere — conditionals, loops, closures. No rules of hooks. Pass them as arguments, store them in dicts, share between islands."))
(~doc-section :title "Island Lifecycle" :id "lifecycle"
(ol :class "space-y-2 text-stone-600 list-decimal list-inside"
(li (strong "Definition: ") (code "defisland") " registers a reactive component (like " (code "defcomp") " + island flag)")
(li (strong "Server render: ") "Body evaluated with initial values. " (code "deref") " returns plain value. Output wrapped in " (code "data-sx-island") " / " (code "data-sx-state"))
(li (strong "Client hydration: ") "Finds " (code "data-sx-island") " elements, creates signals from serialized state, re-renders in reactive context")
(li (strong "Updates: ") "Signal changes update only subscribed DOM nodes. No full island re-render")
(li (strong "Disposal: ") "Island removed from DOM — all signals and effects cleaned up via " (code "with-island-scope"))))
(~doc-section :title "Implementation Status" :id "status"
(p :class "text-stone-600 mb-3" "All signal logic lives in " (code ".sx") " spec files and is bootstrapped to JavaScript and Python. No SX-specific logic in host languages.")
(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" "Layer")
(th :class "px-3 py-2 font-medium text-stone-600" "Status")
(th :class "px-3 py-2 font-medium text-stone-600" "Files")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Signal runtime spec")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "signals.sx (291 lines)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "defisland special form")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "eval.sx, special-forms.sx, render.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "DOM adapter (reactive rendering)")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx (+140 lines)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "HTML adapter (SSR)")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-html.sx (+65 lines)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "JS bootstrapper")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "bootstrap_js.py, sx-ref.js (4769 lines)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Python bootstrapper")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "bootstrap_py.py, sx_ref.py"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Test suite")
(td :class "px-3 py-2 text-green-700 font-medium" "17/17")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "test-signals.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Named stores (L3)")
(td :class "px-3 py-2 text-green-700 font-medium" "Spec'd")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "signals.sx: def-store, use-store, clear-stores"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Event bridge")
(td :class "px-3 py-2 text-green-700 font-medium" "Spec'd")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "signals.sx: emit-event, on-event, bridge-event"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Client hydration")
(td :class "px-3 py-2 text-green-700 font-medium" "Spec'd")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "boot.sx: sx-hydrate-islands, hydrate-island, dispose-island"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Event bindings")
(td :class "px-3 py-2 text-green-700 font-medium" "Spec'd")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx: :on-click → domListen"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "data-sx-emit processing")
(td :class "px-3 py-2 text-green-700 font-medium" "Spec'd")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "orchestration.sx: process-emit-elements"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Island disposal")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "boot.sx, orchestration.sx: dispose-islands-in pre-swap"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Reactive list")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx: map + deref auto-upgrades"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Input binding")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx: :bind signal, bind-input"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Keyed reconciliation")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx: :key attr, extract-key"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Portals")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx: portal render-dom form"))
(tr
(td :class "px-3 py-2 text-stone-700" "Phase 2 remaining")
(td :class "px-3 py-2 text-stone-500 font-medium" "P2")
(td :class "px-3 py-2 font-mono text-xs text-stone-500"
(a :href "/reactive-islands/phase2" :sx-get "/reactive-islands/phase2" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "Error boundaries + resource + patterns")))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Error boundaries")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx: error-boundary render-dom form"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Resource (async signal)")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "signals.sx: resource, promise-then"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Suspense pattern")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "resource + cond/deref (no special form)"))
(tr
(td :class "px-3 py-2 text-stone-700" "Transition pattern")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "schedule-idle + batch (no special form)"))))))))
;; ---------------------------------------------------------------------------
;; Live demo islands
;; ---------------------------------------------------------------------------
;; 1. Counter — basic signal + effect
(defisland ~demo-counter (&key initial)
(let ((count (signal (or initial 0)))
(doubled (computed (fn () (* 2 (deref count))))))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
(div :class "flex items-center gap-4"
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
:on-click (fn (e) (swap! count dec))
"")
(span :class "text-2xl font-bold text-violet-900 w-12 text-center"
(deref count))
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
:on-click (fn (e) (swap! count inc))
"+"))
(p :class "text-sm text-stone-500 mt-2"
"doubled: " (span :class "font-mono text-violet-700" (deref doubled))))))
;; 2. Temperature converter — computed derived signal
(defisland ~demo-temperature ()
(let ((celsius (signal 20))
(fahrenheit (computed (fn () (+ (* (deref celsius) 1.8) 32)))))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
(div :class "flex items-center gap-3"
(div :class "flex items-center gap-2"
(button :class "px-2 py-1 rounded bg-stone-200 text-stone-700 text-sm hover:bg-stone-300"
:on-click (fn (e) (swap! celsius (fn (c) (- c 5))))
"5")
(span :class "font-mono text-lg font-bold text-violet-900 w-16 text-center"
(deref celsius))
(button :class "px-2 py-1 rounded bg-stone-200 text-stone-700 text-sm hover:bg-stone-300"
:on-click (fn (e) (swap! celsius (fn (c) (+ c 5))))
"+5")
(span :class "text-stone-500" "°C"))
(span :class "text-stone-400" "=")
(span :class "font-mono text-lg font-bold text-violet-900"
(deref fahrenheit))
(span :class "text-stone-500" "°F")))))
;; 3. Imperative counter — shows create-text-node + effect pattern
(defisland ~demo-imperative ()
(let ((count (signal 0))
(text-node (create-text-node "0"))
(_eff (effect (fn ()
(dom-set-text-content text-node (str (deref count)))))))
(div :class "rounded border border-stone-200 bg-stone-50 p-4 my-4"
(p :class "text-sm text-stone-600 mb-2" "Imperative style — explicit " (code "effect") " + " (code "create-text-node") ":")
(div :class "flex items-center gap-4"
(button :class "px-3 py-1 rounded bg-stone-600 text-white text-sm font-medium hover:bg-stone-700"
:on-click (fn (e) (swap! count dec))
"")
(span :class "text-2xl font-bold text-stone-900 w-12 text-center"
text-node)
(button :class "px-3 py-1 rounded bg-stone-600 text-white text-sm font-medium hover:bg-stone-700"
:on-click (fn (e) (swap! count inc))
"+")))))
;; 4. Stopwatch — effect with cleanup (interval), fully imperative
(defisland ~demo-stopwatch ()
(let ((running (signal false))
(elapsed (signal 0))
(time-text (create-text-node "0.0s"))
(btn-text (create-text-node "Start"))
;; Timer effect — creates/clears interval based on running signal
(_e1 (effect (fn ()
(when (deref running)
(let ((id (set-interval (fn () (swap! elapsed inc)) 100)))
(fn () (clear-interval id)))))))
;; Display effect
(_e2 (effect (fn ()
(let ((e (deref elapsed)))
(dom-set-text-content time-text
(str (floor (/ e 10)) "." (mod e 10) "s"))))))
;; Button label effect
(_e3 (effect (fn ()
(dom-set-text-content btn-text
(if (deref running) "Stop" "Start"))))))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
(div :class "flex items-center gap-4"
(span :class "font-mono text-2xl font-bold text-violet-900 w-24 text-center"
time-text)
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
:on-click (fn (e) (swap! running not))
btn-text)
(button :class "px-3 py-1 rounded bg-stone-300 text-stone-700 text-sm hover:bg-stone-400"
:on-click (fn (e)
(do (reset! running false)
(reset! elapsed 0)))
"Reset")))))
;; 5. Reactive list — map over a signal, auto-updates when signal changes
(defisland ~demo-reactive-list ()
(let ((next-id (signal 1))
(items (signal (list)))
(add-item (fn (e)
(batch (fn ()
(swap! items (fn (old)
(append old (dict "id" (deref next-id)
"text" (str "Item " (deref next-id))))))
(swap! next-id inc)))))
(remove-item (fn (id)
(swap! items (fn (old)
(filter (fn (item) (not (= (get item "id") id))) old))))))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
(div :class "flex items-center gap-3 mb-3"
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
:on-click add-item
"Add Item")
(span :class "text-sm text-stone-500"
(deref (computed (fn () (len (deref items))))) " items"))
(ul :class "space-y-1"
(map (fn (item)
(li :key (str (get item "id"))
:class "flex items-center justify-between bg-white rounded px-3 py-2 text-sm"
(span (get item "text"))
(button :class "text-stone-400 hover:text-red-500 text-xs"
:on-click (fn (e) (remove-item (get item "id")))
"✕")))
(deref items))))))
;; 6. Input binding — two-way signal binding for form elements
(defisland ~demo-input-binding ()
(let ((name (signal ""))
(agreed (signal false)))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4 space-y-3"
(div :class "flex items-center gap-3"
(input :type "text" :bind name
:placeholder "Type your name..."
:class "px-3 py-1.5 rounded border border-stone-300 text-sm focus:outline-none focus:border-violet-400 w-48")
(span :class "text-sm text-stone-600"
"Hello, "
(strong (deref name))
"!"))
(div :class "flex items-center gap-2"
(input :type "checkbox" :bind agreed :id "agree-cb"
:class "rounded border-stone-300")
(label :for "agree-cb" :class "text-sm text-stone-600" "I agree to the terms"))
(when (deref agreed)
(p :class "text-sm text-green-700" "Thanks for agreeing!")))))
;; 7. Portal — render into a remote DOM target
(defisland ~demo-portal ()
(let ((open? (signal false)))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
:on-click (fn (e) (swap! open? not))
(if (deref open?) "Close Modal" "Open Modal"))
(portal "#portal-root"
(when (deref open?)
(div :class "fixed inset-0 bg-black/50 flex items-center justify-center z-50"
:on-click (fn (e) (reset! open? false))
(div :class "bg-white rounded-lg p-6 max-w-md shadow-xl"
:on-click (fn (e) (stop-propagation e))
(h2 :class "text-lg font-bold text-stone-800 mb-2" "Portal Modal")
(p :class "text-stone-600 text-sm mb-4"
"This content is rendered into " (code "#portal-root") " — outside the island's DOM subtree. It escapes overflow:hidden, z-index stacking, and layout constraints.")
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
:on-click (fn (e) (reset! open? false))
"Close"))))))))
;; 8. Error boundary — catch errors, render fallback with retry
(defisland ~demo-error-boundary ()
(let ((throw? (signal false)))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
(div :class "flex items-center gap-3 mb-3"
(button :class "px-3 py-1 rounded bg-red-600 text-white text-sm font-medium hover:bg-red-700"
:on-click (fn (e) (reset! throw? true))
"Trigger Error")
(button :class "px-3 py-1 rounded bg-stone-300 text-stone-700 text-sm hover:bg-stone-400"
:on-click (fn (e) (reset! throw? false))
"Reset"))
(error-boundary
;; Fallback: receives (err retry-fn)
(fn (err retry-fn)
(div :class "p-3 bg-red-50 border border-red-200 rounded"
(p :class "text-red-700 font-medium text-sm" "Caught: " (error-message err))
(button :class "mt-2 px-3 py-1 rounded bg-red-600 text-white text-sm hover:bg-red-700"
:on-click (fn (e) (do (reset! throw? false) (invoke retry-fn)))
"Retry")))
;; Children: the happy path
(do
(when (deref throw?)
(error "Intentional explosion!"))
(p :class "text-sm text-green-700"
"Everything is fine. Click \"Trigger Error\" to throw."))))))
;; 9. Refs — imperative DOM access via :ref attribute
(defisland ~demo-refs ()
(let ((my-ref (dict "current" nil))
(msg (signal "")))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4 space-y-3"
(input :ref my-ref :type "text" :placeholder "I can be focused programmatically"
:class "px-3 py-1.5 rounded border border-stone-300 text-sm focus:outline-none focus:border-violet-400 w-64")
(div :class "flex items-center gap-3"
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
:on-click (fn (e)
(dom-focus (get my-ref "current")))
"Focus Input")
(button :class "px-3 py-1 rounded bg-stone-300 text-stone-700 text-sm hover:bg-stone-400"
:on-click (fn (e)
(let ((el (get my-ref "current")))
(reset! msg (str "Tag: " (dom-tag-name el)
", value: \"" (dom-get-prop el "value") "\""))))
"Read Input"))
(when (not (= (deref msg) ""))
(p :class "text-sm text-stone-600 font-mono" (deref msg))))))
;; 10. Dynamic class/style — computed signals drive class and style reactively
(defisland ~demo-dynamic-class ()
(let ((danger (signal false))
(size (signal 16)))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4 space-y-3"
(div :class "flex items-center gap-3"
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
:on-click (fn (e) (swap! danger not))
(if (deref danger) "Safe mode" "Danger mode"))
(button :class "px-3 py-1 rounded bg-stone-300 text-stone-700 text-sm hover:bg-stone-400"
:on-click (fn (e) (swap! size (fn (s) (+ s 2))))
"Bigger")
(button :class "px-3 py-1 rounded bg-stone-300 text-stone-700 text-sm hover:bg-stone-400"
:on-click (fn (e) (swap! size (fn (s) (max 10 (- s 2)))))
"Smaller"))
(div :class (str "p-3 rounded font-medium transition-colors "
(if (deref danger) "bg-red-100 text-red-800" "bg-green-100 text-green-800"))
:style (str "font-size:" (deref size) "px")
"This element's class and style are reactive."))))
;; 11. Resource + suspense pattern — async data with loading/error states
(defisland ~demo-resource ()
(let ((data (resource (fn ()
;; Simulate async fetch with a delayed promise
(promise-delayed 1500 (dict "name" "Ada Lovelace"
"role" "First Programmer"
"year" 1843))))))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
(cond
(get (deref data) "loading")
(div :class "flex items-center gap-2 text-stone-500"
(span :class "inline-block w-4 h-4 border-2 border-stone-300 border-t-violet-600 rounded-full animate-spin")
(span :class "text-sm" "Loading..."))
(get (deref data) "error")
(div :class "p-3 bg-red-50 border border-red-200 rounded"
(p :class "text-red-700 text-sm" "Error: " (get (deref data) "error")))
:else
(let ((d (get (deref data) "data")))
(div :class "space-y-1"
(p :class "font-bold text-stone-800" (get d "name"))
(p :class "text-sm text-stone-600" (get d "role") " (" (get d "year") ")")))))))
;; 12. Transition pattern — deferred updates for expensive operations
(defisland ~demo-transition ()
(let ((query (signal ""))
(all-items (list "Signals" "Effects" "Computed" "Batch" "Stores"
"Islands" "Portals" "Error Boundaries" "Resources"
"Input Binding" "Keyed Lists" "Event Bridge"
"Reactive Text" "Reactive Attrs" "Reactive Fragments"
"Disposal" "Hydration" "CSSX" "Macros" "Refs"))
(filtered (signal (list)))
(pending (signal false)))
;; Set initial filtered list
(reset! filtered all-items)
;; Filter effect — defers via schedule-idle so typing stays snappy
(effect (fn ()
(let ((q (lower (deref query))))
(if (= q "")
(do (reset! pending false)
(reset! filtered all-items))
(do (reset! pending true)
(schedule-idle (fn ()
(batch (fn ()
(reset! filtered
(filter (fn (item) (contains? (lower item) q)) all-items))
(reset! pending false))))))))))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4 space-y-3"
(div :class "flex items-center gap-3"
(input :type "text" :bind query :placeholder "Filter features..."
:class "px-3 py-1.5 rounded border border-stone-300 text-sm focus:outline-none focus:border-violet-400 w-48")
(when (deref pending)
(span :class "text-xs text-stone-400" "Filtering...")))
(ul :class "space-y-1"
(map (fn (item)
(li :key item :class "text-sm text-stone-700 bg-white rounded px-3 py-1.5"
item))
(deref filtered))))))
;; 13. Shared stores — cross-island state via def-store / use-store
(defisland ~demo-store-writer ()
(let ((store (def-store "demo-theme" (fn ()
(dict "color" (signal "violet")
"dark" (signal false))))))
(div :class "rounded border border-stone-200 bg-stone-50 p-4 my-2"
(p :class "text-xs font-semibold text-stone-500 mb-2" "Island A — Store Writer")
(div :class "flex items-center gap-3"
(select :bind (get store "color")
:class "px-2 py-1 rounded border border-stone-300 text-sm"
(option :value "violet" "Violet")
(option :value "blue" "Blue")
(option :value "green" "Green")
(option :value "red" "Red"))
(label :class "flex items-center gap-1 text-sm text-stone-600"
(input :type "checkbox" :bind (get store "dark")
:class "rounded border-stone-300")
"Dark mode")))))
(defisland ~demo-store-reader ()
(let ((store (use-store "demo-theme")))
(div :class "rounded border border-stone-200 bg-stone-50 p-4 my-2"
(p :class "text-xs font-semibold text-stone-500 mb-2" "Island B — Store Reader")
(div :class (str "p-3 rounded font-medium text-sm "
(if (deref (get store "dark"))
(str "bg-" (deref (get store "color")) "-900 text-" (deref (get store "color")) "-100")
(str "bg-" (deref (get store "color")) "-100 text-" (deref (get store "color")) "-800")))
"Styled by signals from Island A"))))
;; ---------------------------------------------------------------------------
;; Demo page — shows what's been implemented
;; ---------------------------------------------------------------------------

View File

@@ -0,0 +1,58 @@
;; ---------------------------------------------------------------------------
;; Named Stores — page-level signal containers
;; ---------------------------------------------------------------------------
(defcomp ~reactive-islands-named-stores-content ()
(~doc-page :title "Named Stores"
(~doc-section :title "The Problem" :id "problem"
(p "Islands are isolated by default. Signal props work when islands are adjacent, but not when they are:")
(ul :class "space-y-1 text-stone-600 list-disc pl-5"
(li "Distant in the DOM tree (header badge + drawer island)")
(li "Defined in different " (code ".sx") " files")
(li "Destroyed and recreated by htmx swaps"))
(p "Named stores solve all three. A store is a named collection of signals that lives at " (em "page") " scope, not island scope."))
(~doc-section :title "def-store / use-store" :id "api"
(~doc-code :code (highlight ";; Create a named store — called once at page level\n;; The init function creates signals and computeds\n(def-store \"cart\" (fn ()\n (let ((items (signal (list))))\n (dict\n :items items\n :count (computed (fn () (length (deref items))))\n :total (computed (fn () (reduce + 0\n (map (fn (i) (get i \"price\")) (deref items)))))))))\n\n;; Use the store from any island — returns the signal dict\n(defisland ~cart-badge ()\n (let ((store (use-store \"cart\")))\n (span :class \"badge bg-violet-100 text-violet-800 px-2 py-1 rounded-full\"\n (deref (get store \"count\")))))\n\n(defisland ~cart-drawer ()\n (let ((store (use-store \"cart\")))\n (div :class \"p-4\"\n (h2 \"Cart\")\n (ul (map (fn (item)\n (li :class \"flex justify-between py-1\"\n (span (get item \"name\"))\n (span :class \"text-stone-500\" \"\\u00A3\" (get item \"price\"))))\n (deref (get store \"items\"))))\n (div :class \"border-t pt-2 font-semibold\"\n \"Total: \\u00A3\" (deref (get store \"total\"))))))" "lisp"))
(p (code "def-store") " is " (strong "idempotent") " — calling it again with the same name returns the existing store. This means multiple components can call " (code "def-store") " defensively without double-creating."))
(~doc-section :title "Lifecycle" :id "lifecycle"
(ol :class "space-y-2 text-stone-600 list-decimal list-inside"
(li (strong "Page load: ") (code "def-store") " creates the store in a global registry. Signals are initialized.")
(li (strong "Island hydration: ") "Each island calls " (code "use-store") " to get the shared signal dict. Derefs create subscriptions.")
(li (strong "Island swap: ") "An island is destroyed by htmx swap. Its effects are cleaned up. But the store " (em "persists") " — it's in the page-level registry, not the island scope.")
(li (strong "Island recreation: ") "The new island calls " (code "use-store") " again. Gets the same signals. Reconnects reactively. User state is preserved.")
(li (strong "Full page navigation: ") (code "clear-stores") " wipes the registry. Clean slate.")))
(~doc-section :title "Combining with event bridge" :id "combined"
(p "Named stores + event bridge = full lake→island→island communication:")
(~doc-code :code (highlight ";; Store persists across island lifecycle\n(def-store \"cart\" (fn () ...))\n\n;; Island 1: product page with htmx lake\n(defisland ~product-island ()\n (let ((store (use-store \"cart\")))\n ;; Bridge server-rendered \"Add\" buttons to store\n (bridge-event container \"cart:add\" (get store \"items\")\n (fn (detail) (append (deref (get store \"items\")) detail)))\n ;; Lake content swapped via sx-get\n (div :id \"product-content\" :sx-get \"/products/featured\")))\n\n;; Island 2: cart badge in header (distant in DOM)\n(defisland ~cart-badge ()\n (let ((store (use-store \"cart\")))\n (span (deref (get store \"count\")))))" "lisp"))
(p "User clicks \"Add to Cart\" in server-rendered product content. " (code "cart:add") " event fires. Product island catches it via bridge. Store's " (code "items") " signal updates. Cart badge — in a completely different island — updates reactively because it reads the same signal."))
(~doc-section :title "Spec" :id "spec"
(p "Named stores are spec'd in " (code "signals.sx") " (section 12). Three functions:")
(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" "Function")
(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 font-mono text-sm text-violet-700" "(def-store name init-fn)")
(td :class "px-3 py-2 text-stone-700" "Create or return existing named store. init-fn returns a dict of signals/computeds."))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "(use-store name)")
(td :class "px-3 py-2 text-stone-700" "Get existing store by name. Errors if store doesn't exist."))
(tr
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "(clear-stores)")
(td :class "px-3 py-2 text-stone-700" "Wipe all stores. Called on full page navigation."))))))))
;; ---------------------------------------------------------------------------
;; Plan — the full design document (moved from plans section)
;; ---------------------------------------------------------------------------

View File

@@ -0,0 +1,188 @@
;; ---------------------------------------------------------------------------
;; Phase 2 Plan — remaining reactive features
;; ---------------------------------------------------------------------------
(defcomp ~reactive-islands-phase2-content ()
(~doc-page :title "Phase 2: Completing the Reactive Toolkit"
(~doc-section :title "Where we are" :id "where"
(p "Phase 1 delivered the core reactive primitive: signals, effects, computed values, islands, disposal, stores, event bridges, and reactive DOM rendering. These are sufficient for any isolated interactive widget.")
(p "Phase 2 fills the gaps that appear when you try to build " (em "real application UI") " with islands — forms, modals, dynamic styling, efficient lists, error handling, and async loading. Each feature is independently valuable and independently shippable. None requires changes to the signal runtime.")
(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" "Feature")
(th :class "px-3 py-2 font-medium text-stone-600" "React equiv.")
(th :class "px-3 py-2 font-medium text-stone-600" "Priority")
(th :class "px-3 py-2 font-medium text-stone-600" "Spec file")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Input binding")
(td :class "px-3 py-2 text-stone-500 text-xs" "controlled inputs")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Keyed reconciliation")
(td :class "px-3 py-2 text-stone-500 text-xs" "key prop")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Portals")
(td :class "px-3 py-2 text-stone-500 text-xs" "createPortal")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Error boundaries")
(td :class "px-3 py-2 text-stone-500 text-xs" "componentDidCatch")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Suspense")
(td :class "px-3 py-2 text-stone-500 text-xs" "Suspense + lazy")
(td :class "px-3 py-2 text-stone-500 font-medium" "N/A")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "covered by existing primitives"))
(tr
(td :class "px-3 py-2 text-stone-700" "Transitions")
(td :class "px-3 py-2 text-stone-500 text-xs" "startTransition")
(td :class "px-3 py-2 text-stone-500 font-medium" "N/A")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "covered by existing primitives"))))))
;; -----------------------------------------------------------------------
;; P0 — must have
;; -----------------------------------------------------------------------
(~doc-section :title "P0: Input Binding" :id "input-binding"
(p "You cannot build a form without two-way input binding. React uses controlled components — value is always driven by state, onChange feeds back. SX needs the same pattern but with signals instead of setState.")
(~doc-subsection :title "Design"
(p "A new " (code ":bind") " attribute on " (code "input") ", " (code "textarea") ", and " (code "select") " elements. It takes a signal and creates a bidirectional link: signal value flows into the element, user input flows back into the signal.")
(~doc-code :code (highlight ";; Bind a signal to an input\n(defisland ~login-form ()\n (let ((email (signal \"\"))\n (password (signal \"\")))\n (form :on-submit (fn (e)\n (dom-prevent-default e)\n (fetch-json \"POST\" \"/api/login\"\n (dict \"email\" (deref email)\n \"password\" (deref password))))\n (input :type \"email\" :bind email\n :placeholder \"Email\")\n (input :type \"password\" :bind password\n :placeholder \"Password\")\n (button :type \"submit\" \"Log in\"))))" "lisp"))
(p "The " (code ":bind") " attribute is handled in " (code "adapter-dom.sx") "'s element rendering. For a signal " (code "s") ":")
(ol :class "space-y-1 text-stone-600 list-decimal list-inside text-sm"
(li "Set the element's " (code "value") " to " (code "(deref s)") " initially")
(li "Create an effect: when " (code "s") " changes externally, update " (code "el.value"))
(li "Add an " (code "input") " event listener: on user input, call " (code "(reset! s el.value)"))
(li "For checkboxes/radios: bind to " (code "checked") " instead of " (code "value"))
(li "For select: bind to " (code "value") ", handle " (code "change") " event")))
(~doc-subsection :title "Spec changes"
(~doc-code :code (highlight ";; In adapter-dom.sx, inside render-dom-element:\n;; After processing :on-* event attrs, check for :bind\n(when (dict-has? kwargs \"bind\")\n (let ((sig (dict-get kwargs \"bind\")))\n (when (signal? sig)\n (bind-input el sig))))\n\n;; New function in adapter-dom.sx:\n(define bind-input\n (fn (el sig)\n (let ((tag (lower (dom-tag-name el)))\n (is-checkbox (or (= (dom-get-attr el \"type\") \"checkbox\")\n (= (dom-get-attr el \"type\") \"radio\"))))\n ;; Set initial value\n (if is-checkbox\n (dom-set-prop el \"checked\" (deref sig))\n (dom-set-prop el \"value\" (str (deref sig))))\n ;; Signal → element (effect, auto-tracked)\n (effect (fn ()\n (if is-checkbox\n (dom-set-prop el \"checked\" (deref sig))\n (let ((v (str (deref sig))))\n (when (!= (dom-get-prop el \"value\") v)\n (dom-set-prop el \"value\" v))))))\n ;; Element → signal (event listener)\n (dom-listen el (if is-checkbox \"change\" \"input\")\n (fn (e)\n (if is-checkbox\n (reset! sig (dom-get-prop el \"checked\"))\n (reset! sig (dom-get-prop el \"value\"))))))))" "lisp"))
(p "Platform additions: " (code "dom-set-prop") " and " (code "dom-get-prop") " (property access, not attribute — " (code ".value") " not " (code "getAttribute") "). These go in the boundary as IO primitives."))
(~doc-subsection :title "Derived patterns"
(p "Input binding composes with everything already built:")
(ul :class "space-y-1 text-stone-600 list-disc pl-5 text-sm"
(li (strong "Validation: ") (code "(computed (fn () (>= (len (deref email)) 3)))") " — derived from the bound signal")
(li (strong "Debounced search: ") "Effect with " (code "set-timeout") " cleanup, reading the bound signal")
(li (strong "Form submission: ") (code "(deref email)") " in the submit handler gives the current value")
(li (strong "Stores: ") "Bind to a store signal — multiple islands share the same form state"))))
(~doc-section :title "P0: Keyed List Reconciliation" :id "keyed-list"
(p (code "reactive-list") " currently clears all DOM nodes and re-renders from scratch on every signal change. This works for small lists but breaks down for large ones — focus is lost, animations restart, scroll position resets.")
(~doc-subsection :title "Design"
(p "When items have a " (code ":key") " attribute (or a key function), " (code "reactive-list") " should reconcile by key instead of clearing.")
(~doc-code :code (highlight ";; Keyed list — items matched by :key, reused across updates\n(defisland ~todo-list ()\n (let ((items (signal (list\n (dict \"id\" 1 \"text\" \"Buy milk\")\n (dict \"id\" 2 \"text\" \"Write spec\")\n (dict \"id\" 3 \"text\" \"Ship it\")))))\n (ul\n (map (fn (item)\n (li :key (get item \"id\")\n (span (get item \"text\"))\n (button :on-click (fn (e) ...)\n \"Remove\")))\n (deref items)))))" "lisp"))
(p "The reconciliation algorithm:")
(ol :class "space-y-1 text-stone-600 list-decimal list-inside text-sm"
(li "Extract key from each rendered child (from " (code ":key") " attr or item identity)")
(li "Build a map of " (code "old-key → DOM node") " from previous render")
(li "Walk new items: if key exists in old map, " (strong "reuse") " the DOM node (move to correct position). If not, render fresh.")
(li "Remove DOM nodes whose keys are absent from the new list")
(li "Result: minimum DOM mutations. Focus, scroll, animations preserved.")))
(~doc-subsection :title "Spec changes"
(~doc-code :code (highlight ";; In adapter-dom.sx, replace reactive-list's effect body:\n(define reactive-list\n (fn (map-fn items-sig env ns)\n (let ((marker (create-comment \"island-list\"))\n (key-map (dict)) ;; key → DOM node\n (key-order (list))) ;; current key order\n (effect (fn ()\n (let ((parent (dom-parent marker))\n (items (deref items-sig)))\n (when parent\n (let ((new-map (dict))\n (new-keys (list)))\n ;; Render or reuse each item\n (for-each (fn (item)\n (let ((rendered (render-item map-fn item env ns))\n (key (or (dom-get-attr rendered \"key\")\n (dom-get-data rendered \"key\")\n (identity-key item))))\n (dom-remove-attr rendered \"key\")\n (if (dict-has? key-map key)\n ;; Reuse existing\n (dict-set! new-map key (dict-get key-map key))\n ;; New node\n (dict-set! new-map key rendered))\n (append! new-keys key)))\n items)\n ;; Remove stale nodes\n (for-each (fn (k)\n (when (not (dict-has? new-map k))\n (dom-remove (dict-get key-map k))))\n key-order)\n ;; Reorder to match new-keys\n (let ((cursor marker))\n (for-each (fn (k)\n (let ((node (dict-get new-map k)))\n (when (not (= node (dom-next-sibling cursor)))\n (dom-insert-after cursor node))\n (set! cursor node)))\n new-keys))\n ;; Update state\n (set! key-map new-map)\n (set! key-order new-keys))))))\n marker)))" "lisp"))
(p "Falls back to current clear-and-rerender when no keys are present.")))
;; -----------------------------------------------------------------------
;; P1 — important
;; -----------------------------------------------------------------------
(~doc-section :title "P1: Portals" :id "portals"
(p "A portal renders children into a DOM node " (em "outside") " the island's subtree. Essential for modals, tooltips, dropdown menus, and toast notifications — anything that must escape overflow:hidden, z-index stacking, or layout constraints.")
(~doc-subsection :title "Design"
(~doc-code :code (highlight ";; portal — render children into a target element\n(defisland ~modal-trigger ()\n (let ((open? (signal false)))\n (div\n (button :on-click (fn (e) (swap! open? not))\n \"Open Modal\")\n\n ;; Portal: children rendered into #modal-root,\n ;; not into this island's DOM\n (portal \"#modal-root\"\n (when (deref open?)\n (div :class \"fixed inset-0 bg-black/50 flex items-center justify-center\"\n (div :class \"bg-white rounded-lg p-6 max-w-md\"\n (h2 \"Modal Title\")\n (p \"This is rendered outside the island's DOM subtree.\")\n (button :on-click (fn (e) (reset! open? false))\n \"Close\"))))))))" "lisp"))
(p "Implementation in " (code "adapter-dom.sx") ":")
(ol :class "space-y-1 text-stone-600 list-decimal list-inside text-sm"
(li (code "portal") " is a new render-dom form (add to " (code "RENDER_DOM_FORMS") " and " (code "dispatch-render-form") ")")
(li "First arg is a CSS selector string for the target container")
(li "Remaining args are children, rendered normally via " (code "render-to-dom"))
(li "Instead of returning the fragment, append it to the resolved target element")
(li "Return a comment marker in the original position (for disposal tracking)")
(li "On island disposal, portal content is removed from the target")))
(~doc-subsection :title "Disposal"
(p "Portals must participate in island disposal. When the island is destroyed, portal content must be removed from its remote target. The " (code "with-island-scope") " mechanism handles this — the portal registers a disposer that removes its children from the target element.")))
;; -----------------------------------------------------------------------
;; P2 — nice to have
;; -----------------------------------------------------------------------
(~doc-section :title "P2: Error Boundaries" :id "error-boundaries"
(p "When an island's rendering or effect throws, the error currently propagates to the top level and may crash other islands. An error boundary catches the error and renders a fallback UI.")
(~doc-subsection :title "Design"
(~doc-code :code (highlight ";; error-boundary — catch errors in island subtrees\n(defisland ~resilient-widget ()\n (error-boundary\n ;; Fallback: shown when children throw\n (fn (err)\n (div :class \"p-4 bg-red-50 border border-red-200 rounded\"\n (p :class \"text-red-700 font-medium\" \"Something went wrong\")\n (p :class \"text-red-500 text-sm\" (error-message err))))\n ;; Children: the happy path\n (do\n (~risky-component)\n (~another-component))))" "lisp"))
(p "Implementation:")
(ol :class "space-y-1 text-stone-600 list-decimal list-inside text-sm"
(li (code "error-boundary") " is a new render-dom form")
(li "First arg: fallback function " (code "(fn (error) ...)") " that returns DOM")
(li "Remaining args: children rendered inside a try/catch")
(li "On error: clear the boundary container, render fallback with the caught error")
(li "Effects within the boundary are disposed on error")
(li "A " (code "retry") " function is passed to the fallback for recovery"))))
(~doc-section :title "P2: Suspense" :id "suspense"
(p "Suspense handles async operations in the render path — data fetching, lazy-loaded components, code splitting. Show a loading placeholder until the async work completes, then swap in the result.")
(~doc-subsection :title "Design"
(~doc-code :code (highlight ";; suspense — async-aware rendering boundary\n(defisland ~user-profile (&key user-id)\n (suspense\n ;; Fallback: shown during loading\n (div :class \"animate-pulse\"\n (div :class \"h-4 bg-stone-200 rounded w-3/4\")\n (div :class \"h-4 bg-stone-200 rounded w-1/2 mt-2\"))\n ;; Children: may contain async operations\n (let ((user (await (fetch-json (str \"/api/users/\" user-id)))))\n (div\n (h2 (get user \"name\"))\n (p (get user \"email\"))))))" "lisp"))
(p "This requires a new primitive concept: a " (strong "resource") " — an async signal that transitions through loading → resolved → error states.")
(~doc-code :code (highlight ";; resource — async signal\n(define resource\n (fn (fetch-fn)\n ;; Returns a signal-like value:\n ;; {:loading true :data nil :error nil} initially\n ;; {:loading false :data result :error nil} on success\n ;; {:loading false :data nil :error err} on failure\n (let ((state (signal (dict \"loading\" true\n \"data\" nil\n \"error\" nil))))\n ;; Kick off the async operation\n (promise-then (fetch-fn)\n (fn (data) (reset! state (dict \"loading\" false\n \"data\" data\n \"error\" nil)))\n (fn (err) (reset! state (dict \"loading\" false\n \"data\" nil\n \"error\" err))))\n state)))" "lisp"))
(p "Suspense is the rendering boundary; resource is the data primitive. Together they give a clean async data story without effects-that-fetch (React's " (code "useEffect") " + " (code "useState") " anti-pattern).")))
(~doc-section :title "P2: Transitions" :id "transitions"
(p "Transitions mark updates as non-urgent. The UI stays interactive during expensive re-renders. React's " (code "startTransition") " defers state updates so that urgent updates (typing, clicking) aren't blocked by slow ones (filtering a large list, rendering a complex subtree).")
(~doc-subsection :title "Design"
(~doc-code :code (highlight ";; transition — non-urgent signal update\n(defisland ~search-results (&key items)\n (let ((query (signal \"\"))\n (filtered (signal items))\n (is-pending (signal false)))\n ;; Typing is urgent — updates immediately\n ;; Filtering is deferred — doesn't block input\n (effect (fn ()\n (let ((q (deref query)))\n (transition is-pending\n (fn ()\n (reset! filtered\n (filter (fn (item)\n (contains? (lower (get item \"name\")) (lower q)))\n items)))))))\n (div\n (input :bind query :placeholder \"Search...\")\n (when (deref is-pending)\n (span :class \"text-stone-400\" \"Filtering...\"))\n (ul (map (fn (item) (li (get item \"name\")))\n (deref filtered))))))" "lisp"))
(p (code "transition") " takes a pending-signal and a thunk. It sets pending to true, schedules the thunk via " (code "requestIdleCallback") " (or " (code "setTimeout 0") " as fallback), then sets pending to false when complete. Signal writes inside the thunk are batched and applied asynchronously.")
(p "This is lower priority because SX's fine-grained updates already avoid the re-render-everything problem that makes transitions critical in React. But for truly large lists or expensive computations, deferral is still valuable.")))
;; -----------------------------------------------------------------------
;; Implementation order
;; -----------------------------------------------------------------------
(~doc-section :title "Implementation Order" :id "order"
(p "Each feature is independent. Suggested order based on dependency and value:")
(ol :class "space-y-3 text-stone-600 list-decimal list-inside"
(li (strong "Input binding") " (P0) — unlocks forms. Smallest change, biggest impact. One new function in adapter-dom.sx, two platform primitives (" (code "dom-set-prop") ", " (code "dom-get-prop") "). Add to demo page immediately.")
(li (strong "Keyed reconciliation") " (P0) — unlocks efficient dynamic lists. Replace reactive-list's effect body. Add " (code ":key") " extraction. No new primitives needed.")
(li (strong "Portals") " (P1) — one new render-dom form. Needs disposal integration. Unlocks modals, tooltips, toasts.")
(li (strong "Error boundaries") " (P2) — one new render-dom form with try/catch. Independent of everything else."))
(p :class "mt-4 text-stone-600" "Every feature follows the same pattern: spec in " (code ".sx") " → bootstrap to JS/Python → add platform primitives → add demo island. No feature requires changes to the signal runtime, the evaluator, or the rendering pipeline. They are all additive."))
(~doc-section :title "What we are NOT building" :id "not-building"
(p "Some React features are deliberately excluded:")
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
(li (strong "Virtual DOM / diffing") " — SX uses fine-grained signals. There is no component re-render to diff against. The " (code "reactive-text") ", " (code "reactive-attr") ", " (code "reactive-fragment") ", and " (code "reactive-list") " primitives update the exact DOM nodes that changed.")
(li (strong "JSX / template compilation") " — SX is interpreted at runtime. No build step. The s-expression syntax " (em "is") " the component tree — there is nothing to compile.")
(li (strong "Server components (React-style)") " — SX already has a richer version. The " (code "aser") " mode evaluates server-side logic and serializes the result as SX wire format. Components can be expanded on the server or deferred to the client. This is more flexible than React's server/client component split.")
(li (strong "Concurrent rendering / fiber") " — React's fiber architecture exists to time-slice component re-renders. SX has no component re-renders to slice. Fine-grained updates are inherently incremental.")
(li (strong "Hooks rules") " — Signals are values, not hooks. No rules about ordering, no conditional creation restrictions, no dependency arrays. This is a feature, not a gap.")))))

View File

@@ -0,0 +1,163 @@
;; ---------------------------------------------------------------------------
;; Plan — the full design document (moved from plans section)
;; ---------------------------------------------------------------------------
(defcomp ~reactive-islands-plan-content ()
(~doc-page :title "Reactive Islands Plan"
(~doc-section :title "Context" :id "context"
(p "SX already has a sliding bar for " (em "where") " rendering happens — server-side HTML, SX wire format for client rendering, or any point between. This is the isomorphism bar. It controls the render boundary.")
(p "There is a second bar, orthogonal to the first: " (em "how state flows.") " On one end, all state lives on the server — every user action is a round-trip, every UI update is a fresh render. This is the htmx model. On the other end, state lives on the client — signals, subscriptions, fine-grained DOM updates without server involvement. This is the React model.")
(p "These two bars are independent. You can have server-rendered HTML with client state (SSR + hydrated React). You can have client-rendered components with server state (current SX). The combination creates four quadrants:")
(div :class "overflow-x-auto mt-4 mb-4"
(table :class "w-full text-sm text-left"
(thead
(tr :class "border-b border-stone-200"
(th :class "py-2 px-3 font-semibold text-stone-700" "")
(th :class "py-2 px-3 font-semibold text-stone-700" "Server State")
(th :class "py-2 px-3 font-semibold text-stone-700" "Client State")))
(tbody :class "text-stone-600"
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-semibold text-stone-700" "Server Rendering")
(td :class "py-2 px-3" "Pure hypermedia (htmx)")
(td :class "py-2 px-3" "SSR + hydrated islands (Next.js)"))
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-semibold text-stone-700" "Client Rendering")
(td :class "py-2 px-3" "SX wire format (current)")
(td :class "py-2 px-3 font-semibold text-violet-700" "Reactive islands (this plan)")))))
(p "Today SX occupies the bottom-left quadrant — client-rendered components with server state. This plan adds the bottom-right: " (strong "reactive islands") " with client-local signals. A page can mix all four quadrants. Most content stays hypermedia. Interactive regions opt into reactivity. The author controls where each component sits on both bars."))
(~doc-section :title "The Spectrum" :id "spectrum"
(p "Four levels of client interactivity. Each is independently valuable. Each is opt-in per component.")
(~doc-subsection :title "Level 0: Pure Hypermedia"
(p "The default. " (code "sx-get") ", " (code "sx-post") ", " (code "sx-swap") ". Server renders everything. Client swaps fragments. No client state. No JavaScript state management. This is where 90% of a typical application should live."))
(~doc-subsection :title "Level 1: Local DOM Operations"
(p "Imperative escape hatches for micro-interactions too small for a server round-trip: toggling a menu, switching a tab, showing a tooltip. " (code "toggle!") ", " (code "set-attr!") ", " (code "on-event") ". No reactive graph. Just do the thing directly."))
(~doc-subsection :title "Level 2: Reactive Islands"
(p (code "defisland") " components with local signals. Fine-grained DOM updates — no virtual DOM, no diffing, no component re-renders. A signal change updates only the DOM nodes that read it. Islands are isolated by default. The server can render their initial state."))
(~doc-subsection :title "Level 3: Connected Islands"
(p "Islands that share state via signal props or named stores (" (code "def-store") " / " (code "use-store") "). Plus event bridges for htmx lake-to-island communication. This is where SX starts to feel like React — but only in the regions that need it. The surrounding page remains hypermedia.")))
(~doc-section :title "htmx Lakes" :id "lakes"
(p "An htmx lake is server-driven content " (em "inside") " a reactive island. The island provides the reactive boundary; the lake content is swapped via " (code "sx-get") "/" (code "sx-post") " like normal hypermedia.")
(p "This works because signals live in JavaScript closures, not in the DOM. When a swap replaces lake content, the island's signals are unaffected. The lake can communicate back to the island via the " (a :href "/reactive-islands/event-bridge" :sx-get "/reactive-islands/event-bridge" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "event bridge") ".")
(~doc-subsection :title "Navigation scenarios"
(div :class "space-y-3"
(div :class "rounded border border-green-200 bg-green-50 p-3"
(div :class "font-semibold text-green-800" "Swap inside island")
(p :class "text-sm text-stone-600 mt-1" "Lake content replaced. Signals survive. Effects can rebind to new DOM. User state intact."))
(div :class "rounded border border-green-200 bg-green-50 p-3"
(div :class "font-semibold text-green-800" "Swap outside island")
(p :class "text-sm text-stone-600 mt-1" "Different part of page updated. Island completely unaffected. User state intact."))
(div :class "rounded border border-amber-200 bg-amber-50 p-3"
(div :class "font-semibold text-amber-800" "Swap replaces island")
(p :class "text-sm text-stone-600 mt-1" "Island disposed. Local signals lost. Named stores persist — new island reconnects via use-store."))
(div :class "rounded border border-stone-200 p-3"
(div :class "font-semibold text-stone-800" "Full page navigation")
(p :class "text-sm text-stone-600 mt-1" "Everything cleared. clean slate. clear-stores wipes the registry.")))))
(~doc-section :title "Reactive DOM Rendering" :id "reactive-rendering"
(p "The existing " (code "renderDOM") " function walks the AST and creates DOM nodes. Inside an island, it becomes signal-aware:")
(~doc-subsection :title "Text bindings"
(~doc-code :code (highlight ";; (span (deref count)) creates:\n;; const text = document.createTextNode(sig.value)\n;; effect(() => text.nodeValue = sig.value)" "lisp"))
(p "Only the text node updates. The span is untouched."))
(~doc-subsection :title "Attribute bindings"
(~doc-code :code (highlight ";; (div :class (str \"panel \" (if (deref open?) \"visible\" \"hidden\")))\n;; effect(() => div.className = ...)" "lisp")))
(~doc-subsection :title "Conditional fragments"
(~doc-code :code (highlight ";; (when (deref show?) (~details)) creates:\n;; A marker comment node, then:\n;; effect(() => show ? insert-after(marker, render(~details)) : remove)" "lisp"))
(p "Equivalent to SolidJS's " (code "Show") " — but falls out naturally from the evaluator."))
(~doc-subsection :title "List rendering"
(~doc-code :code (highlight "(map (fn (item) (li :key (get item \"id\") (get item \"name\")))\n (deref items))" "lisp"))
(p "Keyed elements are reused and reordered. Unkeyed elements are morphed.")))
(~doc-section :title "Status" :id "status"
(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" "Task")
(th :class "px-3 py-2 font-medium text-stone-600" "Status")
(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" "Signal runtime")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "signals.sx: signal, deref, reset!, swap!, computed, effect, batch"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Named stores (L3)")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "signals.sx: def-store, use-store, clear-stores"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Event bridge")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "signals.sx: emit-event, on-event, bridge-event"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Event bindings")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "adapter-dom.sx: :on-click (fn ...) → domListen"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "data-sx-emit")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "orchestration.sx: auto-dispatch custom events from server content"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Client hydration")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "boot.sx: hydrate-island, dispose-island, post-swap wiring"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Bootstrapping")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "All functions transpiled to JS and Python, platform primitives implemented"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Island disposal")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "boot.sx, orchestration.sx: effects/computeds auto-register disposers, pre-swap cleanup"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Reactive list")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "adapter-dom.sx: map + deref auto-upgrades to reactive-list"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Input binding + keyed lists")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "adapter-dom.sx: :bind signal, :key attr"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Portals")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "adapter-dom.sx: portal render-dom form"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Error boundaries")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "adapter-dom.sx: error-boundary render-dom form"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Suspense")
(td :class "px-3 py-2 text-stone-500 font-medium" "N/A")
(td :class "px-3 py-2 text-stone-700" "covered by existing primitives"))
(tr
(td :class "px-3 py-2 text-stone-700" "Transitions")
(td :class "px-3 py-2 text-stone-500 font-medium" "N/A")
(td :class "px-3 py-2 text-stone-700" "covered by existing primitives"))))))
(~doc-section :title "Design Principles" :id "principles"
(ol :class "space-y-3 text-stone-600 list-decimal list-inside"
(li (strong "Islands are opt-in.") " " (code "defcomp") " remains the default. Components are inert unless you choose " (code "defisland") ". No reactive overhead for static content.")
(li (strong "Signals are values, not hooks.") " Create them anywhere. Pass them as arguments. Store them in dicts. No rules about calling order or conditional creation.")
(li (strong "Fine-grained, not component-grained.") " A signal change updates the specific DOM node that reads it. The island does not re-render. There is no virtual DOM and no diffing beyond the morph algorithm already in SxEngine.")
(li (strong "The server is still the authority.") " Islands handle client interactions. The server handles auth, data, routing. The server can push state into islands via OOB swaps. Islands can submit data to the server via " (code "sx-post") ".")
(li (strong "Spec-first.") " Signal semantics live in " (code "signals.sx") ". Bootstrapped to JS and Python. The same primitives will work in future hosts — Go, Rust, native.")
(li (strong "No build step.") " Reactive bindings are created at runtime during DOM rendering. No JSX compilation, no Babel transforms, no Vite plugins."))
(p :class "mt-4" "The recommendation from the " (a :href "/essays/client-reactivity" :class "text-violet-700 underline" "Client Reactivity") " essay was: \"Tier 4 probably never.\" This plan is what happens when the answer changes. The design avoids every footgun that essay warns about — no useState cascading to useEffect cascading to Context cascading to a state management library. Signals are one primitive. Islands are one boundary. The rest is composition."))))
;; ---------------------------------------------------------------------------
;; Phase 2 Plan — remaining reactive features
;; ---------------------------------------------------------------------------