From 2bfae33659b2f3243072b01f930247ce490e3681 Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 26 Mar 2026 08:23:05 +0000 Subject: [PATCH] Document all new SX tools at top of sx-tools page Added 7 new documentation sections with code examples: - Cyst: isolated reactive subtrees that survive parent re-renders - Reactive expressions: auto-wrapping deref-containing exprs in computed - Live island preview: paste defisland, get working reactive preview - HyperSX: indentation-based alternative syntax viewer - Inline test runner: browser-embedded test execution with pass/fail - Test harness: three-layer test infrastructure (core/reactive/web) - Core signals: spec-level reactive primitives, zero platform deps Each section has code examples. The test runner section links to the live temperature converter demo with 5/5 inline tests. Co-Authored-By: Claude Opus 4.6 (1M context) --- sx/sx/sx-tools.sx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sx/sx/sx-tools.sx b/sx/sx/sx-tools.sx index b3f870ee..c81d8bf3 100644 --- a/sx/sx/sx-tools.sx +++ b/sx/sx/sx-tools.sx @@ -1 +1 @@ -(defcomp ~sx-tools/overview-content (&key (title "SX Tools") &rest extra) (~docs/page :title title (p :class "text-stone-500 text-sm italic mb-8" "A structural tree editor for s-expression files — because the thing that reads and edits code should understand the code as a tree, not as a sequence of characters.") (~docs/section :title "The problem" :id "problem" (p "On 25 March 2026, the SX documentation site went blank. The home page stepper widget — a 310-line " (code "defisland") " — failed to render. The cause was a single extra closing parenthesis on line 222 of " (code "home-stepper.sx") ".") (p "That parenthesis closed the " (code "letrec") " bindings list one level too early. Two function definitions — " (code "rebuild-preview") " and " (code "do-back") " — silently became body expressions instead of bindings. They were evaluated and discarded rather than bound in scope. Every subsequent reference to " (code "rebuild-preview") " raised " (code "Undefined symbol") ". The island rendered nothing. The page went white.") (p "Finding this took an hour of systematic debugging: adding trace output to the OCaml " (code "env_has") " function, dumping the scope chain at the point of failure, counting keys in the letrec environment (" (em "12 where there should have been 14") "), and finally writing a paren-depth tracer that walked the file character by character to find where the nesting diverged from expectation.") (p "The fix was removing one character.") (p "This is a class of bug, not an incident. S-expressions encode tree structure in linear text using matched delimiters. When those delimiters are wrong, the meaning of every subsequent expression changes. The error is silent — the parser succeeds, the evaluator runs, the wrong thing happens. The gap between the intended tree and the actual tree is invisible in the source.")) (~docs/section :title "Why raw text fails" :id "why-text-fails" (p "Claude Code reads " (code ".sx") " files as raw text and mentally reconstructs the tree structure by tracking bracket nesting. It does this imperfectly — especially in deep or wide trees where closing parentheses pile up and their correspondence to openers is lost. Consider the end of a complex island:") (~docs/code :src "(set-cookie \"sx-home-stepper\" (freeze-to-sx \"home-stepper\"))))))))") (p "Eight closing parentheses. Which one closes " (code "set-cookie") "? Which closes the " (code "fn") "? Which closes the binding pair? Which closes the letrec bindings list? Answering this requires counting backward through hundreds of lines. Counting is not what language models do well.") (p "The same problem compounds when writing. Claude generates plausible-looking s-expression fragments that are structurally wrong — a paren added, a paren dropped, a level of nesting off. The " (code "str_replace") " tool makes this worse: replacing a string inside a deeply nested form can silently unbalance the surrounding structure in ways that are not visible until the file fails to parse — or worse, parses into a different tree.")) (~docs/section :title "The fix: structural tools" :id "structural-tools" (p "If Claude sees trees when the underlying thing is a tree, both the reading and writing problems disappear. Instead of raw text, Claude gets an annotated tree view with explicit paths:") (~docs/code :src (str "[0] (defisland ~home/stepper\n" " [0,0] ~home/stepper\n" " [0,1] ()\n" " [0,2] (let\n" " [0,2,0] let\n" " [0,2,1] ((source ...) ... (code-tokens ...))\n" " [0,2,2] (letrec\n" " [0,2,2,0] letrec\n" " [0,2,2,1] ((split-tag ...) ... (do-back ...))\n" " [0,2,2,2] (freeze-scope ...)\n" " [0,2,2,3] (let ((saved ...)) ...)\n" " [0,2,2,4] (let ((parsed ...)) ...)\n" " [0,2,2,5] (let ((_eff ...)) (div ...)))))")) (p "The structural correspondence that is invisible in raw text is explicit here. Every node has a path. If " (code "rebuild-preview") " appears at " (code "[0,2,2,2]") " instead of " (code "[0,2,2,1,12]") ", it is immediately obvious that it is a body expression, not a letrec binding. The bug that took an hour to find would be visible on inspection.") (p "For editing, Claude specifies tree operations rather than text replacements:") (~docs/code :src (str ";; Replace a node by path — the fragment is parsed before\n" ";; the file is touched. Bracket errors are impossible.\n" "(replace-node \"home-stepper.sx\" [0,2,2,1,12]\n" " \"(rebuild-preview (fn (target) ...))\")\n" "\n" ";; Insert a new child at a specific position\n" "(insert-child \"home-stepper.sx\" [0,2,2,1] 12\n" " \"(new-function (fn () nil))\")\n" "\n" ";; Delete a node — siblings adjust automatically\n" "(delete-node \"home-stepper.sx\" [0,2,2,3])")) (p "Every write operation parses the new fragment as a complete s-expression " (em "before") " navigating to the target path. If the fragment is malformed, the operation returns an error with the line and column of the parse failure. The source file is never left in a partially-edited state. Bracket mismatches become impossible by construction.")) (~docs/section :title "Architecture" :id "architecture" (p "SX Tools is an SX application. The comprehension and editing logic is written in SX, runs on the OCaml evaluator, and is exposed through two interfaces: an MCP server for Claude Code, and an interactive web application for the developer.") (~docs/code :src (str " ┌─────────────────┐\n" " Claude Code ──▶ │ MCP Server │\n" " │ (OCaml stdio) │\n" " └────────┬─────────┘\n" " │\n" " ┌────────▼─────────┐\n" " │ SX Tree Logic │\n" " │ (comprehend.sx) │\n" " │ (edit.sx) │\n" " └────────┬─────────┘\n" " │\n" " ┌───────────────┼───────────────┐\n" " ▼ ▼ ▼\n" " .sx files Web tree editor Validation\n" " (defisland) reports")) (p "The " (strong "parser") " is " (code "sx-parse") " — the same parser that evaluates SX source. No new parser needed. Round-trip fidelity is inherited from the existing serializer.") (p "The " (strong "tree logic") " lives in " (code "web/lib/tree-tools.sx") ". Pure functions: take a parsed tree, return annotated output or a modified tree. No IO, no side effects.") (p "The " (strong "MCP server") " is a thin OCaml binary (" (code "hosts/ocaml/bin/mcp_tree.ml") ") that reads files, calls the SX tree functions via the OCaml bridge, and writes results. Stdio transport, JSON-RPC, single-threaded.") (p "The " (strong "web editor") " is a " (code "defisland") " at " (code "/sx/(applications.(sx-tools))") " — the page you are reading. Interactive tree visualization with click-to-navigate, path display, and structural editing. Islands and signals make it reactive. It is both a tool and a demonstration of the SX platform.")) (~docs/section :title "Comprehension tools" :id "comprehension" (p "These are the tools Claude uses to " (em "understand") " structure before touching anything. They are read-only and have no side effects. They are not a convenience layer — they are as important as the editing tools.") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Annotated tree view") (p "The primary comprehension tool. Every node gets its path label inline, making tree structure explicit. The structural correspondence that is invisible in raw text is readable by inspection, with no need to count brackets:") (~docs/code :src (str "[0] (defcomp ~card\n" " [0,1] (&key title subtitle &rest children)\n" " [0,2] (div :class \"card\"\n" " [0,2,1] :class\n" " [0,2,2] \"card\"\n" " [0,2,3] (h2 title)\n" " [0,2,4] (when subtitle (p subtitle))\n" " [0,2,5] children))")) (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Structural summary") (p "A folded view for large files, showing shape without detail. Claude orients itself in a 300-line island, identifies the region it needs, then calls " (code "read-subtree") " on just that part:") (~docs/code :src (str "(defisland ~home/stepper [0]\n" " (let [0,2]\n" " ((source ...) (code-tokens ...)) [0,2,1]\n" " (letrec [0,2,2]\n" " ((split-tag ...) ... [0,2,2,1]\n" " (rebuild-preview ...) [0,2,2,1,12]\n" " (do-back ...)) [0,2,2,1,13]\n" " (freeze-scope ...) [0,2,2,2]\n" " (let ((_eff ...)) (div ...)))) [0,2,2,5]")) (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Context view") (p "Given a deep path, shows the enclosing chain back to the root. Essential for working deep in a tree without loading the entire file:") (~docs/code :src (str "Context for [0,2,2,1,12]:\n" " [0] defisland ~home/stepper\n" " [0,2] let ((source ...) ... (code-tokens ...))\n" " [0,2,2] letrec ((split-tag ...) ...)\n" " [0,2,2,1] bindings list (14 pairs)\n" " → [0,2,2,1,12] (rebuild-preview (fn (target) ...))")) (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Bracket-paired view") (p "The raw source annotated with matched pair labels, for cases where Claude needs to see the actual syntax and verify bracket correspondence:") (~docs/code :src (str "(₁defcomp ~card (₂&key title subtitle &rest children)₂\n" " (₃div :class \"card\"\n" " (₄h2 title)₄\n" " (₅when subtitle (₆p subtitle)₆)₅\n" " children)₃)₁"))) (~docs/section :title "Smart tree reading" :id "smart-read" (p "For large files, " (code "sx_read_tree") " automatically manages context size. With no extra parameters, it detects when a file exceeds 200 lines and auto-summarises at depth 2. Four modes give Claude control over how much context to pull in:") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Focus mode") (p "The " (code "focus") " parameter expands only subtrees that match a pattern, collapsing everything else. This is the most useful mode — it combines search and comprehension in one call.") (~docs/code :src (str ";; Show only the parts of the tree that mention \"defisland\"\nsx_read_tree file=\"home.sx\" focus=\"defisland\"\n\n;; The ~docs/section containing defisland expands fully.\n;; Every other section collapses to one line.")) (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Depth limit") (p "The " (code "max_depth") " parameter works like " (code "sx_summarise") " — it caps how deep the tree expands.") (~docs/code :src (str ";; Just the top-level structure\nsx_read_tree file=\"home.sx\" max_depth=1\n\n;; Two levels deep\nsx_read_tree file=\"home.sx\" max_depth=3")) (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Pagination") (p "The " (code "max_lines") " and " (code "offset") " parameters paginate the full annotated tree. The output includes a header showing which lines are displayed and whether more are available.") (~docs/code :src (str ";; First 30 lines\nsx_read_tree file=\"home.sx\" max_lines=30\n\n;; Next 30 lines\nsx_read_tree file=\"home.sx\" max_lines=30 offset=30\n\n;; Output header:\n;; ;; Lines 0-30 of 590 (more available)")) (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Auto mode") (p "With no parameters, files under 200 lines return the full annotated tree. Larger files auto-summarise at depth 2 with a note showing the total line count and how to get more detail.")) (~docs/section :title "Edit operations" :id "editing" (p "All operations take a tree, perform the operation, and return either a new tree or a structured error. Nothing is mutated in place. File writing is a separate step that only happens after the edit succeeds.") (table :class "min-w-full text-sm mb-6" (thead (tr (th :class "text-left pr-4 pb-2 font-semibold text-stone-700" "Operation") (th :class "text-left pb-2 font-semibold text-stone-700" "Description"))) (tbody :class "text-stone-600" (tr (td :class "pr-4 py-1 font-mono text-xs" "replace-node") (td :class "py-1" "Replace the node at a path with new parsed source")) (tr (td :class "pr-4 py-1 font-mono text-xs" "insert-child") (td :class "py-1" "Insert a new child at a specific index within a list")) (tr (td :class "pr-4 py-1 font-mono text-xs" "delete-node") (td :class "py-1" "Remove a node — siblings shift to fill the gap")) (tr (td :class "pr-4 py-1 font-mono text-xs" "wrap-node") (td :class "py-1" "Wrap a node in a new list — e.g. wrap expression in " (code "(when cond ...)"))) (tr (td :class "pr-4 py-1 font-mono text-xs" "validate") (td :class "py-1" "Check structural integrity — balanced parens, valid paths")))) (p (strong "Fragment-first validation.") " Every write operation parses the new source fragment as a complete s-expression before navigating to the target path. If the fragment is malformed, the operation returns an error with the line and column of the parse failure. The source file is never touched in the failure path.") (p (strong "Named paths.") " Index paths break when sibling nodes are inserted or deleted. Named paths — " (code "[Head \"letrec\", Head \"rebuild-preview\"]") " — survive structural edits and are more natural for Claude to reason about. Claude should prefer named paths; index paths are for mechanical follow-up after " (code "find-node") " has located a target.")) (~docs/section :title "Smart editing" :id "smart-editing" (p "The path-based edit tools require knowing the exact path to a node. The smart edit tools combine search with editing — find a node by pattern, then edit it in one call.") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Rename symbol") (p (code "sx_rename_symbol") " renames every occurrence of a symbol throughout a file. Structural — only renames symbol nodes, never touches strings or keywords.") (~docs/code :src (str ";; Rename a component\nsx_rename_symbol file=\"home.sx\" old_name=\"~card\" new_name=\"~ui/card\"\n;; → Renamed 3 occurrences of '~card' → '~ui/card' in home.sx")) (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Replace by pattern") (p (code "sx_replace_by_pattern") " finds the first node matching a pattern and replaces it with new source. Set " (code "all=true") " to replace every match.") (~docs/code :src (str ";; Replace first match\nsx_replace_by_pattern file=\"home.sx\" pattern=\"~old-card\" new_source=\"(~new-card :title t)\"\n\n;; Replace all matches\nsx_replace_by_pattern file=\"home.sx\" pattern=\"~old-card\" new_source=\"(~new-card :title t)\" all=true")) (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Insert near pattern") (p (code "sx_insert_near") " inserts a new form before or after the top-level definition that contains a pattern match. No path arithmetic needed — just name what you want to insert near.") (~docs/code :src (str ";; Insert a new component before the footer definition\nsx_insert_near file=\"page.sx\" pattern=\"~footer\" position=\"before\"\n new_source=\"(defcomp ~sidebar () (div :class sidebar))\"")) (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Rename across files") (p (code "sx_rename_across") " renames a symbol in every " (code ".sx") " file under a directory. Use " (code "dry_run=true") " to preview which files would be affected before committing.") (~docs/code :src (str ";; Preview\nsx_rename_across dir=\"/root/rose-ash/sx\" old_name=\"~card\" new_name=\"~ui/card\" dry_run=true\n;; → home.sx: 3 occurrences (dry run)\n;; → layout.sx: 1 occurrences (dry run)\n\n;; Commit\nsx_rename_across dir=\"/root/rose-ash/sx\" old_name=\"~card\" new_name=\"~ui/card\""))) (~docs/section :title "REPL" :id "repl" (p "The " (code "sx_eval") " tool evaluates SX expressions inside the MCP server's environment. The environment has the parser, tree-tools functions, and all primitives loaded — the same context the tree tools themselves run in.") (p "This is useful for testing expressions, checking how primitives behave, or debugging tree-tool functions without a full build cycle.") (~docs/code :src (str ";; Simple evaluation\n(sx_eval \"(+ 1 2)\") → 3\n\n;; Test tree-tools functions\n(sx_eval \"(path-str (list 0 2 1))\") → \"[0,2,1]\"\n\n;; Parse and inspect\n(sx_eval \"(len (sx-parse \\\"(div (p hello))\\\"))\") → 1")) (p (strong "Limitation: ") "the environment contains parser + tree-tools + primitives. It does not have the full evaluator spec, application components, or macros loaded. Use it for testing pure functions and primitives.")) (~docs/section :title "Project-wide tools" :id "project-wide" (p "These tools search across all " (code ".sx") " files in a directory tree. They answer the questions Claude needs most often: where is something defined, what definitions exist, and who uses a given component.") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Find across files") (p (code "sx_find_across") " runs " (code "find-all") " on every " (code ".sx") " file under a directory. Results include the file path, tree path, and node summary.") (~docs/code :src (str ";; Find all islands in the project\nsx_find_across dir=\"/root/rose-ash/sx\" pattern=\"defisland\"\n\n;; Output:\n;; sx/home.sx [0] (defisland ~home/stepper ...)\n;; sx/sx-tools-editor.sx [0] (defisland ~sx-tools/tree-editor ...)")) (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Component list") (p (code "sx_comp_list") " scans a directory for all top-level definitions: " (code "defcomp") ", " (code "defisland") ", " (code "defmacro") ", " (code "defpage") ", " (code "define") ". Returns a structured table with type, name, file, and parameters.") (~docs/code :src (str ";; List everything defined in sx/\nsx_comp_list dir=\"/root/rose-ash/sx\"\n\n;; Output:\n;; TYPE NAME FILE PARAMS\n;; defcomp ~docs/page sx/docs.sx &key title ...")) (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Component usage") (p (code "sx_comp_usage") " finds every reference to a component or symbol across all " (code ".sx") " files. The reverse of " (code "find-all") " — instead of searching one file, it searches the whole project.") (~docs/code :src (str ";; Who uses ~docs/section?\nsx_comp_usage dir=\"/root/rose-ash/sx\" name=\"~docs/section\"\n\n;; Output:\n;; sx/sx-tools.sx [0,3,4] (~docs/section :title \"The problem\" ...)\n;; sx/sx-tools.sx [0,3,5] (~docs/section :title \"Why raw text fails\" ...)\n;; ..."))) (~docs/section :title "Structural diff" :id "diff" (p (code "sx_diff") " compares two " (code ".sx") " files structurally and reports differences with tree paths. Unlike text diff, it understands nesting — a renamed symbol deep in a tree shows as one " (code "CHANGED") " line, not a confusing hunk of parentheses.") (~docs/code :src (str ";; Compare two versions of a file\nsx_diff file_a=\"home.sx.bak\" file_b=\"home.sx\"\n\n;; Output:\n;; CHANGED [0,2,1] \"old-title\" → \"new-title\"\n;; ADDED [0,3] (p \"new paragraph\")\n;; REMOVED [0,5] (div :class \"obsolete\" ...)")) (p "Three change types: " (code "CHANGED") " (same position, different content), " (code "ADDED") " (present in second file but not first), " (code "REMOVED") " (present in first but not second). Comparison is positional — nodes are matched by index, not by name.")) (~docs/section :title "Development tools" :id "dev-tools" (p "Tools for the SX development workflow: building, testing, formatting, linting, and macro expansion.") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Pretty print") (p (code "sx_pretty_print") " reformats an " (code ".sx") " file with indentation. Short forms stay on one line, longer forms break across lines with keyword args kept paired. All edit tools use the pretty printer automatically when writing files.") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Write file") (p (code "sx_write_file") " creates or overwrites an " (code ".sx") " file. Source is parsed before writing — malformed SX is rejected and the file is not touched. Output is pretty-printed.") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Build") (p (code "sx_build") " builds the SX runtime. Target " (code "js") " (default) builds " (code "sx-browser.js") ", " (code "ocaml") " runs " (code "dune build") ". Set " (code "full=true") " for extensions and type system.") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Test") (p (code "sx_test") " runs the SX test suite and returns a summary with pass/fail counts. Any failures are listed with details. Host is " (code "js") " (default) or " (code "ocaml") ". Set " (code "full=true") " for the extended suite.") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Format check") (p (code "sx_format_check") " lints an " (code ".sx") " file for structural issues: empty " (code "let") " bindings, " (code "defcomp") "/" (code "defisland") " with too few args, " (code "define") " with no body, duplicate parameters.") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Macro expand") (p (code "sx_macroexpand") " evaluates an expression with a file's definitions loaded. Use to test macros — the file's " (code "defmacro") " forms are available in the evaluation environment.")) (~docs/section :title "Git integration" :id "git" (p "Tools that combine git awareness with structural understanding of " (code ".sx") " files.") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Changed files") (p (code "sx_changed") " lists all " (code ".sx") " files changed since a git ref (default: " (code "main") "), with a depth-1 structural summary of each. Use to understand the scope of changes on a branch.") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Branch diff") (p (code "sx_diff_branch") " runs " (code "sx_diff") " on every changed " (code ".sx") " file, comparing the current version against the base ref. Shows structural ADDED/REMOVED/CHANGED per file — like a PR review that understands trees.") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Blame") (p (code "sx_blame") " shows git blame for an " (code ".sx") " file. Optionally takes a tree path to focus on a specific node.") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Documentation generator") (p (code "sx_doc_gen") " scans a directory and generates documentation from all " (code "defcomp") ", " (code "defisland") ", and " (code "defmacro") " signatures — names, types, keyword parameters, and whether the component accepts children.") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Playwright tests") (p (code "sx_playwright") " runs the Playwright browser test suite for the SX docs site. Optionally specify a single spec file. Returns pass/fail summary with failure details.")) (~docs/section :title "The protocol" :id "protocol" (p "The tools only work if they are actually used. The MCP server must be accompanied by a protocol — enforced via " (code "CLAUDE.md") " — that prevents fallback to raw text editing.") (~docs/code :src (str ";; Before doing anything in an .sx file:\n" ";; 1. summarise → structural overview of the whole file\n" ";; 2. read-subtree → expand the region you intend to work in\n" ";; 3. get-context → understand the position of specific nodes\n" ";; 4. find-all → locate definitions or patterns by name\n" "\n" ";; For every edit:\n" ";; 1. read-subtree → confirm the correct path\n" ";; 2. replace-node / insert-child / delete-node / wrap-node\n" ";; 3. validate → confirm structural integrity\n" ";; 4. read-subtree → verify the result\n" "\n" ";; Never use str_replace on .sx files.\n" ";; Never proceed to an edit without first establishing\n" ";; where you are in the tree using the comprehension tools.")) (p "The comprehension-first discipline is the key insight. Claude cannot edit reliably what it does not understand reliably. The same parsed tree representation serves both needs — reading and writing are two sides of the same structural problem.")) (~docs/section :title "Why SX, not OCaml" :id "why-sx" (p "The original plan called for a pure OCaml implementation with " (code "angstrom") " parser combinators and a Wadler-Lindig pretty-printer. This is unnecessary. The SX ecosystem already has everything:") (ul :class "space-y-2 text-stone-600" (li (strong "Parser: ") (code "sx-parse") " already parses s-expressions correctly — it is the same parser the evaluator uses. No second parser to maintain, no divergence risk.") (li (strong "Serializer: ") (code "sx-serialize") " already handles round-tripping. The existing serializer preserves structure.") (li (strong "Tree operations: ") "Recursive list processing is what SX does best. Annotating a tree, folding to a summary, navigating by path — these are all natural " (code "map") "/" (code "reduce") "/" (code "filter") " operations on nested lists.") (li (strong "Web UI: ") "The interactive tree editor is a " (code "defisland") " — signals for selection state, reactive DOM for the tree view, lakes for server-morphable content. The home stepper widget is proof this works.") (li (strong "OCaml host: ") "The SX functions run on the OCaml evaluator. The MCP server is a thin OCaml wrapper around SX function calls. Native performance for the server, WASM for the browser — same codebase.")) (p "Writing the tree tools in SX means they can run in the browser (via the WASM evaluator) and on the server (via the OCaml kernel). The web editor and the MCP server share identical logic. There is one implementation, not two.")) (~docs/section :title "Build plan" :id "build-plan" (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Phase 1 — Tree comprehension functions") (p "Implement " (code "annotate-tree") ", " (code "summarise") ", " (code "read-subtree") ", " (code "get-context") ", " (code "find-all") ", " (code "bracket-pairs") " as pure SX functions in " (code "web/lib/tree-tools.sx") ". Test against real project " (code ".sx") " files. Iterate on output formats until the output is genuinely easy for a language model to read — the format is load-bearing.") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Phase 2 — Edit operations") (p "Implement " (code "replace-node") ", " (code "insert-child") ", " (code "delete-node") ", " (code "wrap-node") ", " (code "validate") " as pure SX functions. Fragment-first validation on all write operations. Test error paths exhaustively — error messages are part of the interface.") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Phase 3 — MCP server") (p "Thin OCaml binary: stdio JSON-RPC, calls SX functions via the bridge, reads/writes files. Wire all comprehension and edit tools to MCP handlers. Manual testing with raw JSON.") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Phase 4 — Web editor") (p (code "defisland ~sx-tools/tree-editor") " — interactive tree visualization on this page. Click a node to see its path, context, and siblings. Edit nodes through a structural interface. Islands and signals for reactivity. A tool and a demonstration.") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Phase 5 — Integration and iteration") (p "Write the " (code "CLAUDE.md") " protocol. Run real tasks with Claude Code — both reading and editing. Observe which comprehension tools Claude actually reaches for. Observe where it still makes structural errors. Iterate on output formats and add any missing tools. The output formats deserve careful design based on observed behaviour, not just on what seems reasonable in advance.")) (~docs/section :title "Try it" :id "try-it" (p "Paste or edit SX source below. The tree view shows every node with its path — click a node to select it, then switch to context view to see the enclosing chain.") (~sx-tools/tree-editor)) (~docs/section :title "What changes" :id "what-changes" (p "With SX Tools, the debugging session that found the home-stepper bug would not have happened. The workflow would have been:") (ul :class "space-y-2 text-stone-600" (li (code "summarise home-stepper.sx") " → see that " (code "rebuild-preview") " is at " (code "[0,4]") " (body expression) instead of " (code "[0,2,2,1,12]") " (letrec binding). The bug is visible on the summary.") (li "Even without noticing the summary, " (code "validate home-stepper.sx") " would report the structural anomaly: 14 names in the letrec bindings list but only 12 binding pairs, with 2 bare expressions following.") (li "The fix — " (code "delete-node") " to remove the extra paren, or " (code "wrap-node") " to restructure — would be a tree operation. No character-counting, no mental stack simulation, no risk of introducing a second paren error while fixing the first.")) (p "The gap between intended tree and actual tree stops being invisible. Claude sees trees, edits trees, and the brackets take care of themselves.")))) +(defcomp ~sx-tools/overview-content (&key (title "SX Tools") &rest extra) (~docs/page :title title (p :class "text-stone-500 text-sm italic mb-8" "A structural tree editor for s-expression files — because the thing that reads and edits code should understand the code as a tree, not as a sequence of characters.") (~docs/section :title "Cyst — isolated reactive subtrees" :id "cyst" (p "A " (code "cyst") " is an isolated reactive scope inside a parent render tree. When the parent re-renders, the cyst's DOM is preserved — event handlers, signal subscriptions, and state all survive.") (~docs/code :src "(cyst :key \"my-counter\"\n (div\n (button :on-click (fn (e) (swap! count inc)) \"+\")\n (span (deref count))))") (p "Without " (code "cyst") ", a reactive island nested inside another island's render tree gets its DOM recreated on every parent re-render. Event handlers are lost, signals reset. " (code "cyst") " solves this by caching the rendered DOM and returning the cached version when the parent re-evaluates.") (p "The " (code ":key") " parameter provides a stable identity for the cached DOM. On first render, the body is evaluated in a fresh " (code "with-island-scope") ". On subsequent renders, if the cached DOM is still connected to the document, it is returned as-is.") (p "This is a language-level feature in " (code "adapter-dom.sx") " — any island can use " (code "cyst") " to protect a reactive subtree from parent re-renders.")) (~docs/section :title "Reactive expressions" :id "reactive-expressions" (p "Expressions containing " (code "(deref sig)") " inside an island scope are now automatically reactive. The DOM adapter detects " (code "deref") " calls within compound expressions and wraps them in " (code "(computed ...)") " for dependency tracking.") (~docs/code :src ";; Before: only bare (deref sig) was reactive\n(span (deref celsius)) ;; ✓ reactive\n\n;; Now: compound expressions are reactive too\n(span (str (deref celsius) \"°C\")) ;; ✓ reactive\n(span (+ (* (deref celsius) 1.8) 32)) ;; ✓ reactive\n(span (str \"Count: \" (deref count) \" × 2 = \" (deref doubled))) ;; ✓ reactive") (p "The " (code "contains-deref?") " predicate recursively checks if an expression tree contains a " (code "deref") " call. When found inside an island scope, the expression is wrapped in " (code "(reactive-text (computed (fn () (eval-expr expr env))))") " — creating a signal subscription that re-evaluates the entire expression when any deref'd signal changes.") (p "This is a language-level feature in " (code "adapter-dom.sx") ". Every island on the site benefits automatically.")) (~docs/section :title "Live island preview" :id "live-preview" (p "The render tab evaluates " (code "defisland") " expressions as live reactive islands. Paste a defisland, click Parse, click render — the island runs with working signals, computed values, and event handlers.") (p "Try this in the editor below:") (~docs/code :src "(defisland ~counter ()\n (let ((count (signal 0))\n (doubled (computed (fn () (* 2 (deref count))))))\n (div\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)))))") (p "The preview uses " (code "cyst") " to preserve the island across parent re-renders. " (code "sx-load-components") " registers the component, then the native " (code "render-dom-island") " adapter renders it with full reactive wiring.") (p "For " (code "defcomp") " with " (code "&key") " parameters, the render tab shows input fields. Values substitute into the preview live as you type — no re-parse needed.")) (~docs/section :title "HyperSX — alternative syntax view" :id "hypersx" (p "The hypersx tab shows SX in an indentation-based syntax inspired by Pug and _hyperscript. Same semantics, different surface:") (~docs/code :src ";; SX\n(defisland ~counter ()\n (let ((count (signal 0)))\n (div :class \"flex gap-4\"\n (button :on-click (fn (e) (swap! count inc)) \"+\")\n (span (str \"Count: \" (deref count))))))\n\n;; HyperSX\ndefisland ~counter ()\n let count = signal(0)\n div.flex.gap-4\n button :on-click (fn (e) (swap! count inc)) \"+\"\n span \"Count: {@count}\"") (p "Transforms: CSS selector shorthand (" (code "div.card#main") "), signal sugar (" (code "@count") ", " (code "signal()") ", " (code ":=") ", " (code "<-") "), string interpolation (" (code "\"{@count}\"") "), and structural keywords (" (code "when") ", " (code "if") ", " (code "let") ", " (code "map") ", " (code "for") ").") (p "Implemented as pure SX in " (code "web/lib/hypersx.sx") ". The " (code "sx->hypersx") " function takes a parsed tree and returns a string. Bidirectional " (code "hypersx->sx") " parser is planned.")) (~docs/section :title "Inline test runner" :id "test-runner" (p "Demo pages can embed tests that run in the browser. Tests are written in SX using the harness assertion functions and stored in a " (code "