From af77fc32c7b57d64a5dcf4abe30607bae4bf2881 Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 5 Mar 2026 13:34:58 +0000 Subject: [PATCH] Move spec metadata from Python to SX, add orchestration to spec viewer Spec file registry (slugs, filenames, titles, descriptions) now lives in nav-data.sx as SX data definitions. Python helper reduced to pure file I/O (read-spec-file). Architecture page updated with engine/orchestration split and dependency graph. Co-Authored-By: Claude Opus 4.6 --- sx/sx/nav-data.sx | 27 ++++++++++++++++- sx/sx/specs.sx | 22 ++++++++++---- sx/sxc/pages/docs.sx | 34 ++++++++++++++------- sx/sxc/pages/helpers.py | 65 ++--------------------------------------- 4 files changed, 69 insertions(+), 79 deletions(-) diff --git a/sx/sx/nav-data.sx b/sx/sx/nav-data.sx index 0f61bd8..07b437b 100644 --- a/sx/sx/nav-data.sx +++ b/sx/sx/nav-data.sx @@ -78,7 +78,32 @@ (dict :label "DOM Adapter" :href "/specs/adapter-dom") (dict :label "HTML Adapter" :href "/specs/adapter-html") (dict :label "SX Wire Adapter" :href "/specs/adapter-sx") - (dict :label "SxEngine" :href "/specs/engine"))) + (dict :label "SxEngine" :href "/specs/engine") + (dict :label "Orchestration" :href "/specs/orchestration"))) + +;; Spec file registry — canonical metadata for spec viewer pages. +;; Python only handles file I/O (read-spec-file); all metadata lives here. + +(define core-spec-items (list + (dict :slug "parser" :filename "parser.sx" :title "Parser" :desc "Tokenization and parsing of SX source text into AST.") + (dict :slug "evaluator" :filename "eval.sx" :title "Evaluator" :desc "Tree-walking evaluation of SX expressions.") + (dict :slug "primitives" :filename "primitives.sx" :title "Primitives" :desc "All built-in pure functions and their signatures.") + (dict :slug "renderer" :filename "render.sx" :title "Renderer" :desc "Shared rendering registries and utilities used by all adapters."))) + +(define adapter-spec-items (list + (dict :slug "adapter-dom" :filename "adapter-dom.sx" :title "DOM Adapter" :desc "Renders SX expressions to live DOM nodes. Browser-only.") + (dict :slug "adapter-html" :filename "adapter-html.sx" :title "HTML Adapter" :desc "Renders SX expressions to HTML strings. Server-side.") + (dict :slug "adapter-sx" :filename "adapter-sx.sx" :title "SX Wire Adapter" :desc "Serializes SX for client-side rendering. Component calls stay unexpanded.") + (dict :slug "engine" :filename "engine.sx" :title "SxEngine" :desc "Pure logic for fetch, swap, history, SSE, triggers, morph, and indicators.") + (dict :slug "orchestration" :filename "orchestration.sx" :title "Orchestration" :desc "Browser wiring that binds engine logic to DOM events, fetch, and lifecycle."))) + +(define all-spec-items (concat core-spec-items adapter-spec-items)) + +(define find-spec + (fn (slug) + (some (fn (item) + (when (= (get item "slug") slug) item)) + all-spec-items))) ;; Find the current nav label for a slug by matching href suffix. ;; Returns the label string or nil if no match. diff --git a/sx/sx/specs.sx b/sx/sx/specs.sx index 4f8bb8b..0289407 100644 --- a/sx/sx/specs.sx +++ b/sx/sx/specs.sx @@ -96,7 +96,11 @@ (p :class "text-stone-600" "The engine is the browser-side fetch/swap/history system. It processes " (code :class "text-violet-700 text-sm" "sx-*") - " attributes on elements to make HTTP requests, swap content, manage browser history, and handle events. It depends on the core evaluator and the DOM adapter.") + " attributes on elements to make HTTP requests, swap content, manage browser history, and handle events. It is split into two files: pure logic (" + (code :class "text-violet-700 text-sm" "engine.sx") + ") and browser wiring (" + (code :class "text-violet-700 text-sm" "orchestration.sx") + ").") (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" @@ -109,7 +113,14 @@ :sx-get "/specs/engine" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" "engine.sx")) - (td :class "px-3 py-2 text-stone-700" "SxEngine — fetch, swap, history, SSE, triggers, indicators")))))) + (td :class "px-3 py-2 text-stone-700" "Pure logic — trigger parsing, swap algorithms, morph, history, SSE, indicators")) + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 font-mono text-sm text-violet-700" + (a :href "/specs/orchestration" :class "hover:underline" + :sx-get "/specs/orchestration" :sx-target "#main-panel" :sx-select "#main-panel" + :sx-swap "outerHTML" :sx-push-url "true" + "orchestration.sx")) + (td :class "px-3 py-2 text-stone-700" "Browser wiring — binds engine to DOM events, fetch, request lifecycle")))))) (div :class "space-y-3" (h2 :class "text-2xl font-semibold text-stone-800" "Dependency graph") @@ -124,7 +135,8 @@ adapter-dom.sx depends on: render, eval adapter-html.sx depends on: render, eval adapter-sx.sx depends on: render, eval -engine.sx depends on: eval, adapter-dom"))) +engine.sx depends on: eval, adapter-dom +orchestration.sx depends on: engine, adapter-dom"))) (div :class "space-y-3" (h2 :class "text-2xl font-semibold text-stone-800" "Self-hosting") @@ -143,14 +155,14 @@ engine.sx depends on: eval, adapter-dom"))) ;; Overview pages (Core / Adapters) — show truncated previews of each file ;; --------------------------------------------------------------------------- -(defcomp ~spec-overview-content (&key spec-files) +(defcomp ~spec-overview-content (&key spec-title spec-files) (~doc-page :title (or spec-title "Specs") (p :class "text-stone-600 mb-6" (case spec-title "Core Language" "The core specification defines the language itself — parsing, evaluation, primitives, and shared rendering definitions. These four files are platform-independent and sufficient to implement SX on any target." "Adapters & Engine" - "Adapters connect the core language to specific environments. Each adapter takes evaluated expression trees and produces output for its target. The engine adds browser-side fetch/swap behaviour." + "Adapters connect the core language to specific environments. Each adapter takes evaluated expression trees and produces output for its target. The engine adds browser-side fetch/swap behaviour, split into pure logic and browser orchestration." :else "")) (div :class "space-y-8" (map (fn (spec) diff --git a/sx/sxc/pages/docs.sx b/sx/sxc/pages/docs.sx index 9041617..1762897 100644 --- a/sx/sxc/pages/docs.sx +++ b/sx/sxc/pages/docs.sx @@ -268,14 +268,26 @@ :sub-nav (~section-nav :items specs-nav-items :current (find-current specs-nav-items slug)) :selected (or (find-current specs-nav-items slug) "")) - :data (spec-data slug) - :content (if spec-not-found - (~spec-not-found :slug slug) - (case slug - "core" (~spec-overview-content :spec-files spec-files) - "adapters" (~spec-overview-content :spec-files spec-files) - :else (~spec-detail-content - :spec-title spec-title - :spec-desc spec-desc - :spec-filename spec-filename - :spec-source spec-source)))) + :content (case slug + "core" (~spec-overview-content + :spec-title "Core Language" + :spec-files (map (fn (item) + (dict :title (get item "title") :desc (get item "desc") + :filename (get item "filename") :href (str "/specs/" (get item "slug")) + :source (read-spec-file (get item "filename")))) + core-spec-items)) + "adapters" (~spec-overview-content + :spec-title "Adapters & Engine" + :spec-files (map (fn (item) + (dict :title (get item "title") :desc (get item "desc") + :filename (get item "filename") :href (str "/specs/" (get item "slug")) + :source (read-spec-file (get item "filename")))) + adapter-spec-items)) + :else (let ((spec (find-spec slug))) + (if spec + (~spec-detail-content + :spec-title (get spec "title") + :spec-desc (get spec "desc") + :spec-filename (get spec "filename") + :spec-source (read-spec-file (get spec "filename"))) + (~spec-not-found :slug slug))))) diff --git a/sx/sxc/pages/helpers.py b/sx/sxc/pages/helpers.py index 1d0bec5..3f88908 100644 --- a/sx/sxc/pages/helpers.py +++ b/sx/sxc/pages/helpers.py @@ -16,7 +16,7 @@ def _register_sx_helpers() -> None: "primitives-data": _primitives_data, "reference-data": _reference_data, "attr-detail-data": _attr_detail_data, - "spec-data": _spec_data, + "read-spec-file": _read_spec_file, }) @@ -104,72 +104,13 @@ def _reference_data(slug: str) -> dict: } -_CORE_SPECS = { - "parser": ("parser.sx", "Parser", "Tokenization and parsing of SX source text into AST."), - "evaluator": ("eval.sx", "Evaluator", "Tree-walking evaluation of SX expressions."), - "primitives": ("primitives.sx", "Primitives", "All built-in pure functions and their signatures."), - "renderer": ("render.sx", "Renderer", "Shared rendering registries and utilities used by all adapters."), -} - -_ADAPTER_SPECS = { - "adapter-dom": ("adapter-dom.sx", "DOM Adapter", "Renders SX expressions to live DOM nodes. Browser-only."), - "adapter-html": ("adapter-html.sx", "HTML Adapter", "Renders SX expressions to HTML strings. Server-side."), - "adapter-sx": ("adapter-sx.sx", "SX Wire Adapter", "Serializes SX for client-side rendering. Component calls stay unexpanded."), - "engine": ("engine.sx", "SxEngine", "Fetch/swap/history engine for browser-side SX. Like HTMX but native to SX."), -} - -_ALL_SPECS = {**_CORE_SPECS, **_ADAPTER_SPECS} - - -def _spec_data(slug: str) -> dict: - """Return spec file source and metadata for display.""" +def _read_spec_file(filename: str) -> str: + """Read a spec .sx file from the ref directory. Pure I/O — metadata lives in .sx.""" import os - ref_dir = os.path.join(os.path.dirname(__file__), "..", "..", "shared", "sx", "ref") if not os.path.isdir(ref_dir): ref_dir = "/app/shared/sx/ref" - - base = {"spec-not-found": None, "spec-title": None, "spec-desc": None, - "spec-filename": None, "spec-source": None, "spec-files": None} - - if slug == "core": - specs = [] - for key in ("parser", "evaluator", "primitives", "renderer"): - filename, title, desc = _CORE_SPECS[key] - filepath = os.path.join(ref_dir, filename) - source = _read_spec(filepath) - specs.append({ - "title": title, "desc": desc, "filename": filename, - "source": source, "href": f"/specs/{key}", - }) - return {**base, "spec-title": "Core Language", "spec-files": specs} - - if slug == "adapters": - specs = [] - for key in ("adapter-dom", "adapter-html", "adapter-sx", "engine"): - filename, title, desc = _ADAPTER_SPECS[key] - filepath = os.path.join(ref_dir, filename) - source = _read_spec(filepath) - specs.append({ - "title": title, "desc": desc, "filename": filename, - "source": source, "href": f"/specs/{key}", - }) - return {**base, "spec-title": "Adapters & Engine", "spec-files": specs} - - info = _ALL_SPECS.get(slug) - if not info: - return {**base, "spec-not-found": True} - - filename, title, desc = info filepath = os.path.join(ref_dir, filename) - source = _read_spec(filepath) - return {**base, - "spec-title": title, "spec-desc": desc, - "spec-filename": filename, "spec-source": source} - - -def _read_spec(filepath: str) -> str: - """Read a spec file, returning empty string if missing.""" try: with open(filepath, encoding="utf-8") as f: return f.read()