;; SX Expression URLs — GraphSX ;; Plan: replace path-based routing with s-expression URLs where the URL ;; IS the query, the render instruction, and the address — all at once. (defcomp ~plans/sx-urls/plan-sx-urls-content () (~docs/page :title "SX Expression URLs" (~docs/section :title "Vision" :id "vision" (p "URLs become s-expressions. The entire routing layer collapses into eval. " "Every page is a function, every URL is a function call, and the nav tree hierarchy " "is encoded directly in the nesting of the expression.") (p "Current URLs like " (code "/language/docs/introduction") " become " (code "/sx/(language.(doc.introduction))") ". Dots replace spaces as the URL-friendly " "separator — they are unreserved in RFC 3986, never percent-encoded, and visually clean. " "The parser treats dot as whitespace: " (code "s/./ /") " before parsing as SX.") (~docs/code :src (highlight ";; Current → SX URLs (dots = spaces)\n/language/specs/signals → /(language.(spec.signals))\n/language/specs/explore/signals → /(language.(spec.(explore.signals)))\n/language/docs/introduction → /(language.(doc.introduction))\n/etc/plans/spec-explorer → /(etc.(plan.spec-explorer))\n\n;; Direct component access — any defcomp is addressable\n/(~essays/sx-sucks/essay-sx-sucks)\n/(~plans/sx-urls/plan-sx-urls-content)\n/(~analyzer/bundle-analyzer-content)" "lisp"))) (~docs/section :title "Scoping — The 30-Year Ambiguity, Fixed" :id "scoping" (p "REST URLs have an inherent ambiguity: does a filter/parameter apply to " "the last segment, or the whole path? Consider:") (~docs/code :src (highlight ";; REST — ambiguous:\n/users/123/posts?filter=published\n;; Is the filter scoped to posts? Or to the user? Or the whole query?\n;; Nobody knows. Conventions vary. Documentation required.\n\n;; SX URLs — explicit scoping via nesting:\n/(hello.(sailor.(filter.hhh))) ;; filter scoped to sailor\n/(hello.sailor.(filter.hhh)) ;; filter scoped to hello\n\n;; These mean different things, both expressible.\n;; Parens make scope visible. No ambiguity. No documentation needed." "lisp")) (p "This is not a minor syntactic preference. REST has never been able to express " "the difference between a parameter scoped to a sub-resource and one scoped to the " "parent. Query strings are flat — " (code "?filter=x") " applies to... what, exactly? " "The last path segment? The whole URL? It depends on the API.") (p "S-expression nesting makes scope " (em "structural") ". " "The paren boundaries are the scope boundaries. " "What took REST 30 years of convention documents to approximate, " "SX URLs express in the syntax itself.")) (~docs/section :title "Dots as URL-Safe Whitespace" :id "dots" (p "Spaces in URLs are ugly — they become " (code "%20") " in copy-paste, curl, logs, and proxies. " "Dots are unreserved in RFC 3986, never encoded, and read naturally as \"drill down.\"") (p "The rule is simple: " (strong "dot = space, nothing more") ". " "Parens carry all the structural meaning. Dots are syntactic sugar for URLs only:") (~docs/code :src (highlight ";; These are identical after dot→space transform:\n/(language.(doc.introduction)) → (language (doc introduction))\n/(geography.(hypermedia.(reference.attributes)))\n → (geography (hypermedia (reference attributes)))\n\n;; Parens are still required for nesting:\n/(language.doc.introduction) → (language doc introduction)\n;; = language(\"doc\", \"introduction\") — WRONG\n\n;; Correct nesting:\n/(language.(doc.introduction)) → (language (doc introduction))\n;; = language(doc(\"introduction\")) — RIGHT" "lisp")) (p "The server's URL handler does one thing before parsing: " (code "url_expr = raw_path[1:].replace('.', ' ')") ". Then standard SX parsing takes over.")) (~docs/section :title "The Lisp Tax" :id "parens" (p "People will hate the parentheses. But consider what developers already accept:") (~docs/code :src (highlight ";; Developers happily write this every day:\nhttps://api.site.com/v2/users/123/posts?filter=published&sort=date&order=desc&limit=10&offset=20\n\n;; And they would complain about this?\nhttps://site.com/(users.(posts.123.(filter.published.sort.date.limit.10)))\n\n;; The second is shorter, structured, unambiguous, and composable." "lisp")) (p "The real question: who is reading these URLs?") (ul :class "space-y-2 text-stone-600 list-disc pl-5" (li (strong "End users") " barely look at URLs anymore — they live in address bars people ignore") (li (strong "Developers") " will love it once they get it — it is powerful and consistent") (li (strong "Crawlers and bots") " do not care at all")) (p "And this site is " (em "about SX, implemented in SX") ". " "The audience is exactly the people who will love the parens. " "Every URL on the site is a live example of SX in action. " "Visiting a page is evaluating an expression.")) (~docs/section :title "The Site Is a REPL" :id "repl" (p "The address bar becomes the input line of a REPL. The page is the output.") (~docs/code :src (highlight "/sx/(about) ;; renders the about page\n/(source.(about)) ;; returns the SX source for the about page\n/(eval.(source.(about))) ;; re-evaluates it live\n\n;; The killer demo:\n/(eval.(map.double.(list.1.2.3))) ;; actually returns (2 4 6)\n\n;; The website IS a REPL. The address bar IS the input." "lisp")) (p "You do not need to explain what SX is. You show someone a URL and they " "immediately understand the entire language and philosophy. " "The whole site becomes a self-hosting proof of concept — " "that is not just elegant, that is the pitch.")) (~docs/section :title "Components as Query Resolvers" :id "resolvers" (p "The " (code "~") " sigil means \"find and execute this component.\" " "Components can make onward queries, process results, and return composed content — " "like server-side includes but Lispy and composable.") (~docs/code :src (highlight ";; ~get is a component that fetches, processes, and returns\n/(~get.everything-under-the-sun)\n\n;; The flow:\n;; 1. Server finds ~get component in env\n;; 2. ~get makes onward queries\n;; 3. Processes and transforms results\n;; 4. Returns composed hypermedia\n\n;; Because it's all SX, you nest and compose:\n/(~page.home\n .(~hero.banner)\n .(~get.latest-posts.(limit.5))\n .(~get.featured.(filter.pinned)))\n\n;; Each ~component is independently:\n;; - cacheable (by its expression)\n;; - reusable (same component, different args)\n;; - testable (evaluate in isolation)" "lisp")) (p "This is what React Server Components are trying to do — server-side data resolution " "composed with client-side rendering. Except React needs a framework, a bundler, " "a serialization protocol, and \"use server\" pragmas. " "SX gets it from a sigil and an evaluator.")) (~docs/section :title "HTTP Semantics — REST Re-Aligned" :id "http" (p "GraphQL uses POST for queries even though they are pure reads — " "because queries can be long and the body feels more natural for structured data. " "But this violates HTTP semantics: POST implies side effects, " "GET requests are cacheable, bookmarkable, shareable.") (p "SX URLs naturally align with HTTP because the query " (em "is") " the URL:") (div :class "overflow-x-auto mt-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" "Method") (th :class "py-2 px-3 font-semibold text-stone-700" "Semantics") (th :class "py-2 px-3 font-semibold text-stone-700" "Example"))) (tbody :class "text-stone-600" (tr :class "border-b border-stone-100" (td :class "py-2 px-3 font-semibold" "GET") (td :class "py-2 px-3" "Pure evaluation — cacheable, bookmarkable") (td :class "py-2 px-3 font-mono text-sm" "GET /(language.(doc.intro))")) (tr :class "border-b border-stone-100" (td :class "py-2 px-3 font-semibold" "POST") (td :class "py-2 px-3" "Side effects — mutations, submissions") (td :class "py-2 px-3 font-mono text-sm" "POST /(submit-post)"))))) (p "CDN caching of Lisp hypermedia just works. " "This is what REST always wanted but GraphQL abandoned. " "SX re-aligns with HTTP while being more powerful than both.")) (~docs/section :title "GraphSX — This Is a Query Language" :id "graphsx" (p "The SX URL scheme is not just a routing convention — it is the emergence of " (strong "GraphSX") ": GraphQL but Lisp. The structural parallel is exact:") (div :class "overflow-x-auto mt-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" "Concept") (th :class "py-2 px-3 font-semibold text-stone-700" "GraphQL") (th :class "py-2 px-3 font-semibold text-stone-700" "GraphSX"))) (tbody :class "text-stone-600" (tr :class "border-b border-stone-100" (td :class "py-2 px-3 font-semibold" "Single endpoint") (td :class "py-2 px-3" (code "/graphql")) (td :class "py-2 px-3" "Catch-all " (code "/"))) (tr :class "border-b border-stone-100" (td :class "py-2 px-3 font-semibold" "Query structure") (td :class "py-2 px-3" "Nested fields " (code "{ language { doc { ... } } }")) (td :class "py-2 px-3" "Nested s-expressions " (code "(language.(doc....))"))) (tr :class "border-b border-stone-100" (td :class "py-2 px-3 font-semibold" "Resolvers") (td :class "py-2 px-3" "Per-field functions") (td :class "py-2 px-3" "Page/section functions + " (code "~components"))) (tr :class "border-b border-stone-100" (td :class "py-2 px-3 font-semibold" "Fragments") (td :class "py-2 px-3" "Named reusable selections") (td :class "py-2 px-3" "Components (" (code "defcomp") ")")) (tr :class "border-b border-stone-100" (td :class "py-2 px-3 font-semibold" "Schema") (td :class "py-2 px-3" "Type definitions") (td :class "py-2 px-3" "Page function registry + component env")) (tr :class "border-b border-stone-100" (td :class "py-2 px-3 font-semibold" "Scoping") (td :class "py-2 px-3" "Flat — " (code "?filter=x") " applies to... what?") (td :class "py-2 px-3" "Structural — parens ARE scope boundaries")) (tr :class "border-b border-stone-100" (td :class "py-2 px-3 font-semibold" "Transport") (td :class "py-2 px-3" "POST JSON (violates HTTP GET semantics)") (td :class "py-2 px-3" "GET " (code "/sx/(expr)") " — cacheable, bookmarkable")) (tr :class "border-b border-stone-100" (td :class "py-2 px-3 font-semibold" "Response") (td :class "py-2 px-3" "JSON data (needs separate rendering)") (td :class "py-2 px-3" "Content " (em "and") " data — response is already meaningful"))))) (p "The killer difference: in GraphQL, query and rendering are separate concerns — " "you fetch JSON, then a frontend renders it. In GraphSX, " (strong "the query language and the rendering language are the same thing") ". " (code "(language.(doc.introduction))") " is simultaneously:") (ol :class "space-y-1 text-stone-600 list-decimal pl-5" (li "A " (strong "query") " — resolve the docs/introduction content within the language section") (li "A " (strong "render instruction") " — render it inside a language section wrapper with nav context") (li "A " (strong "URL") " — addressable, bookmarkable, shareable, cacheable")) (p "This is what Lisp does. The uniform syntax means there is no boundary between " "routing DSL, query language, template language, and component system. " "They are all s-expressions evaluated in the same environment. " "GraphQL had to invent a special syntax for queries because JSON is data, not code. " "S-expressions are both.")) (~docs/section :title "Direct Component URLs" :id "components" (p "Any " (code "defcomp") " is directly addressable via its " (code "~plans/content-addressed-components/name") ". " "The URL evaluator sees " (code "~essays/sx-sucks/essay-sx-sucks") ", looks it up in the component env, " "evaluates it, wraps in " (code "~layouts/doc") ", and returns.") (~docs/code :src (highlight ";; Page functions are convenience wrappers:\n/(etc.(essay.sx-sucks)) ;; dispatches via case statement\n\n;; But you can bypass them entirely:\n/(~essays/sx-sucks/essay-sx-sucks) ;; direct component — no routing needed\n\n;; Every defcomp is instantly URL-accessible:\n/(~plans/sx-urls/plan-sx-urls-content) ;; this very page\n/(~analyzer/bundle-analyzer-content) ;; tools\n/(~docs-content/docs-evaluator-content) ;; docs" "lisp")) (p "New components are instantly URL-accessible without routing wiring. " "Debugging is trivial — render any component in isolation.")) (~docs/section :title "URL Special Forms" :id "special-forms" (p "URL-level functions that transform how content is resolved or displayed:") (div :class "overflow-x-auto mt-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" "Form") (th :class "py-2 px-3 font-semibold text-stone-700" "Example") (th :class "py-2 px-3 font-semibold text-stone-700" "Effect"))) (tbody :class "text-stone-600" (tr :class "border-b border-stone-100" (td :class "py-2 px-3 font-mono text-violet-700" "source") (td :class "py-2 px-3 font-mono text-sm" "/sx/(source.(~essays/sx-sucks/essay-sx-sucks))") (td :class "py-2 px-3" "Show defcomp source instead of rendering")) (tr :class "border-b border-stone-100" (td :class "py-2 px-3 font-mono text-violet-700" "inspect") (td :class "py-2 px-3 font-mono text-sm" "/sx/(inspect.(language.(doc.primitives)))") (td :class "py-2 px-3" "Deps, CSS classes, render plan, IO requirements")) (tr :class "border-b border-stone-100" (td :class "py-2 px-3 font-mono text-violet-700" "diff") (td :class "py-2 px-3 font-mono text-sm" "/sx/(diff.(spec.signals).(spec.eval))") (td :class "py-2 px-3" "Side-by-side two pages")) (tr :class "border-b border-stone-100" (td :class "py-2 px-3 font-mono text-violet-700" "search") (td :class "py-2 px-3 font-mono text-sm" "/sx/(search.\"define\".:in.(spec.signals))") (td :class "py-2 px-3" "Grep within a page or spec")) (tr :class "border-b border-stone-100" (td :class "py-2 px-3 font-mono text-violet-700" "raw") (td :class "py-2 px-3 font-mono text-sm" "/sx/(raw.(~some-component))") (td :class "py-2 px-3" "Skip ~layouts/doc nav wrapping")) (tr :class "border-b border-stone-100" (td :class "py-2 px-3 font-mono text-violet-700" "eval") (td :class "py-2 px-3 font-mono text-sm" "/sx/(eval.(map.double.(list.1.2.3)))") (td :class "py-2 px-3" "Arbitrary evaluation — the URL bar is a REPL")) (tr :class "border-b border-stone-100" (td :class "py-2 px-3 font-mono text-violet-700" "json") (td :class "py-2 px-3 font-mono text-sm" "/sx/(json.(language.(doc.primitives)))") (td :class "py-2 px-3" "Return data as JSON — pure query mode")))))) (~docs/section :title "Evaluation Model" :id "eval" (p "The URL path (after stripping " (code "/") " and replacing dots with spaces) " "is parsed as SX and evaluated with a " (strong "soft eval") ": " "known function names are called; unknown symbols self-evaluate to their name as a string; " "components (" (code "~plans/content-addressed-components/name") ") are looked up in the component env.") (~docs/code :src (highlight "/sx/(language.(doc.introduction))\n\n;; After dot→space: (language (doc introduction))\n;; 1. Eval `introduction` → not a known function → \"introduction\"\n;; 2. Eval (doc \"introduction\") → call doc(\"introduction\") → page content\n;; 3. Eval (language content) → call language(content) → passes through\n;; 4. Router wraps result in (~layouts/doc :path \"(language (doc introduction))\" ...)\n\n/(~essays/sx-sucks/essay-sx-sucks)\n;; 1. Eval ~essays/sx-sucks/essay-sx-sucks → component lookup → evaluate → content\n;; 2. Router wraps in ~layouts/doc" "lisp")) (~docs/subsection :title "Section Functions" (p "Structural functions that encode hierarchy and pass through content:") (~docs/code :src (highlight "(define language\n (fn (&rest args)\n (if (empty? args) (language-index-content) (first args))))\n\n(define geography\n (fn (&rest args)\n (if (empty? args) (geography-index-content) (first args))))\n\n;; Sub-sections also pass through\n(define hypermedia\n (fn (&rest args)\n (if (empty? args) (hypermedia-index-content) (first args))))" "lisp"))) (~docs/subsection :title "Page Functions" (p "Leaf functions that dispatch to content components. " "Data-dependent pages call helpers directly — the async evaluator handles IO:") (~docs/code :src (highlight "(define doc\n (fn (&rest args)\n (let ((slug (first-or-nil args)))\n (if (nil? slug)\n (~docs-content/docs-introduction-content)\n (case slug\n \"introduction\" (~docs-content/docs-introduction-content)\n \"getting-started\" (~docs-content/docs-getting-started-content)\n ...)))))\n\n(define bootstrapper\n (fn (&rest args)\n (let ((slug (first-or-nil args))\n (data (when slug (bootstrapper-data slug))))\n (if (nil? slug)\n (~specs/bootstrappers-index-content)\n (if (get data \"bootstrapper-not-found\")\n (~specs/not-found :slug slug)\n (case slug\n \"python\" (~specs/bootstrapper-py-content ...)\n ...))))))" "lisp")))) (~docs/section :title "The Catch-All Route" :id "route" (p "The entire routing layer becomes one handler:") (~docs/code :src (highlight "@app.get(\"/\")\nasync def sx_home():\n return await eval_sx_url(\"/\")\n\n@app.get(\"/\")\nasync def sx_eval_route(expr):\n return await eval_sx_url(f\"/{expr}\")" "python")) (p (code "eval_sx_url") " in seven steps:") (ol :class "space-y-1 text-stone-600 list-decimal pl-5" (li "URL-decode the path") (li "Replace dots with spaces") (li "Parse as SX expression") (li "Auto-quote unknown symbols (slugs become strings)") (li "Evaluate with components + helpers + page/section functions in env") (li "Wrap result in " (code "~layouts/doc") " with the URL expression as " (code ":path")) (li "Return HTML or SX wire format depending on HTMX request")) (p "Defhandler API endpoints and Python demo routes are registered " (em "before") " the catch-all, " "so they match first.")) (~docs/section :title "Composability" :id "composability" (~docs/code :src (highlight ";; Direct component access\n/(~essays/sx-sucks/essay-sx-sucks)\n/(~specs-explorer/spec-explorer-content)\n\n;; URL special forms\n/(source.(~essays/sx-sucks/essay-sx-sucks)) ;; view defcomp source\n/(inspect.(language.(doc.primitives))) ;; deps, render plan\n/(diff.(language.(spec.signals)).(language.(spec.eval))) ;; side by side\n/(eval.(map.double.(list.1.2.3))) ;; REPL in the URL bar\n\n;; Components as query resolvers\n/(~page.home\n .(~hero.banner)\n .(~get.latest-posts.(limit.5))\n .(~get.featured.(filter.pinned)))\n\n;; Scoping is explicit\n/(users.(posts.123.(filter.published))) ;; filter scoped to posts\n/(users.posts.123.(filter.published)) ;; filter scoped to users\n\n;; Cross-service (future)\n/(market.(product.42.:fields.(name.price)))\n/(subscribe.(etc.(plan.status)))" "lisp"))) (~docs/section :title "Implementation Phases" :id "phases" (div :class "space-y-4" (div :class "rounded border border-violet-200 bg-violet-50 p-4" (p :class "font-semibold text-violet-800 mb-2" "Phase 1: Page Functions + Catch-All Route") (ul :class "list-disc pl-5 text-stone-700 space-y-1" (li "Create " (code "page-functions.sx") " — ~20 section + ~15 page functions") (li "Create " (code "sx_router.py") " — URL parser (dot→space), soft eval, streaming detection") (li "Replace " (code "auto_mount_pages") " with catch-all") (li "Direct " (code "~component") " URL support"))) (div :class "rounded border border-blue-200 bg-blue-50 p-4" (p :class "font-semibold text-blue-800 mb-2" "Phase 2: Navigation Data") (ul :class "list-disc pl-5 text-stone-700 space-y-1" (li "Rewrite ~200 " (code ":href") " values to dot-separated SX URLs") (li "Rewrite " (code "resolve-nav-path") " for tree-descent matching"))) (div :class "rounded border border-emerald-200 bg-emerald-50 p-4" (p :class "font-semibold text-emerald-800 mb-2" "Phase 3: Backward Compatibility") (ul :class "list-disc pl-5 text-stone-700 space-y-1" (li "301 redirects from all old paths to new SX URLs") (li "Algorithmic pattern matching — ~25 regex rules"))) (div :class "rounded border border-amber-200 bg-amber-50 p-4" (p :class "font-semibold text-amber-800 mb-2" "Phase 4: URL Special Forms") (ul :class "list-disc pl-5 text-stone-700 space-y-1" (li (code "source") ", " (code "inspect") ", " (code "diff") ", " (code "raw") ", " (code "eval") ", " (code "json")) (li "Components as query resolvers (" (code "~get") ", " (code "~page") ")"))) (div :class "rounded border border-rose-200 bg-rose-50 p-4" (p :class "font-semibold text-rose-800 mb-2" "Phase 5: Client-Side Routing") (ul :class "list-disc pl-5 text-stone-700 space-y-1" (li "Update " (code "_build_pages_sx()") " for function-based page registry") (li "Client-side dot→space→parse→eval pipeline") (li "Rebootstrap " (code "sx-ref.js") " and " (code "sx_ref.py")))) (div :class "rounded border border-stone-200 bg-stone-50 p-4" (p :class "font-semibold text-stone-800 mb-2" "Phase 6: Cleanup") (ul :class "list-disc pl-5 text-stone-700 space-y-1" (li "Delete " (code "docs.sx") " (all 46 defpages)") (li "Grep content files for stale old-style hrefs"))))) (~docs/section :title "What Stays the Same" :id "unchanged" (ul :class "space-y-2 text-stone-600 list-disc pl-5" (li (strong "Defhandler API paths") " — registered before the catch-all, match first") (li (strong "Python demo routes") " — registered via blueprint before the catch-all") (li (strong "All other services") " — blog, market, etc. keep defpage routing") (li (strong "Component definitions") " — all " (code ".sx") " files unchanged") (li (strong "Page helpers") " — called from page functions instead of defpage " (code ":data")) (li (strong "Layout, CSS, shell, rendering pipeline") " — no changes")))))