Files
rose-ash/sx/sx/plans/sx-urls.sx
giles 6f96452f70 Fix empty code blocks: rename ~docs/code param, fix batched IO dispatch
Two bugs caused code blocks to render empty across the site:

1. ~docs/code component had parameter named `code` which collided with
   the HTML <code> tag name. Renamed to `src` and updated all 57
   callers. Added font-mono class for explicit monospace.

2. Batched IO dispatch in ocaml_bridge.py only skipped one leading
   number (batch ID) but the format has two (epoch + ID):
   (io-request EPOCH ID "name" args...). Changed to skip all leading
   numbers so the string name is correctly found. This fixes highlight
   and other batchable helpers returning empty results.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 18:08:40 +00:00

303 lines
24 KiB
Plaintext

;; 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 "/<path:expr>")))
(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(\"/<path:expr>\")\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")))))