diff --git a/sx/sx/sx-tools-demos.sx b/sx/sx/sx-tools-demos.sx new file mode 100644 index 00000000..2e16161c --- /dev/null +++ b/sx/sx/sx-tools-demos.sx @@ -0,0 +1,128 @@ +;; Interactive demos for sx-tree tools. +;; Each demo runs client-side using the same SX functions the MCP server uses. + +(defisland ~sx-tools/tool-playground () + (let ((source (signal "(defcomp ~card (&key title subtitle &rest children)\n (div :class \"card\"\n (h2 title)\n (when subtitle (p :class \"sub\" subtitle))\n children))")) + (tool (signal "summarise")) + (output (signal "")) + (parsed (signal nil))) + + (letrec + ((do-parse (fn () + (reset! parsed (sx-parse (deref source))))) + + (fmt-path (fn (path) + (str "[" (join "," (map str path)) "]"))) + + (node-preview (fn (node depth) + (cond + (nil? node) "nil" + (string? node) (str "\"" (if (> (len node) 30) (str (substring node 0 30) "...") node) "\"") + (number? node) (str node) + (= (type-of node) "boolean") (if node "true" "false") + (= (type-of node) "symbol") (symbol-name node) + (= (type-of node) "keyword") (str ":" (keyword-name node)) + (list? node) (if (empty? node) "()" + (str "(" (node-preview (first node) 0) + (if (> (len node) 1) (str " ... " (len node) " children") "") ")")) + :else (str node)))) + + (summarise-node (fn (node path depth max-depth) + (if (not (list? node)) + (str (fmt-path path) " " (node-preview node 0) "\n") + (if (or (empty? node) (>= depth max-depth)) + (str (fmt-path path) " " (node-preview node 0) "\n") + (str (fmt-path path) " (" (node-preview (first node) 0) " ... " (len node) " children)\n" + (join "" (map-indexed (fn (i child) + (summarise-node child (concat path (list (+ i 1))) (+ depth 1) max-depth)) + (rest node)))))))) + + (annotate-node (fn (node path depth) + (if (not (list? node)) + (str (fmt-path path) " " (node-preview node 0) "\n") + (if (empty? node) (str (fmt-path path) " ()\n") + (str (fmt-path path) " (" (node-preview (first node) 0) + (if (and (<= (len node) 3) (not (some list? (rest node)))) + (str " " (join " " (map (fn (c) (node-preview c 0)) (rest node))) ")\n") + (str "\n" (join "" (map-indexed (fn (i child) + (annotate-node child (concat path (list (+ i 1))) (+ depth 1))) + (rest node))) ")\n"))))))) + + (find-pattern (fn (tree pattern path results) + (when (list? tree) + (when (and (not (empty? tree)) + (= (type-of (first tree)) "symbol") + (contains? (symbol-name (first tree)) pattern)) + (append! results (str (fmt-path path) " " (node-preview tree 0)))) + (for-each (fn (i) + (let ((child (nth tree i))) + (when (list? child) + (find-pattern child pattern (concat path (list i)) results)))) + (range 0 (len tree)))))) + + (get-context (fn (tree path) + (let ((result (list))) + (for-each (fn (depth) + (let ((prefix (slice path 0 (+ depth 1))) + (node (reduce (fn (cur idx) (if (and (list? cur) (< idx (len cur))) (nth cur idx) nil)) tree prefix))) + (when node + (append! result (str (if (= depth (- (len path) 1)) "→ " " ") + (fmt-path prefix) " " (node-preview node 0)))))) + (range 0 (len path))) + result))) + + (run-tool (fn () + (do-parse) + (let ((tree (deref parsed)) + (t (deref tool))) + (when tree + (reset! output + (cond + (= t "summarise") + (join "" (map-indexed (fn (i expr) + (summarise-node expr (list i) 0 2)) + tree)) + (= t "read-tree") + (join "" (map-indexed (fn (i expr) + (annotate-node expr (list i) 0)) + tree)) + (= t "find-all") + (let ((results (list))) + (for-each (fn (i) (find-pattern (nth tree i) "def" (list i) results)) + (range 0 (len tree))) + (if (empty? results) "No matches for \"def\"" + (join "\n" results))) + (= t "validate") + (if (and tree (not (empty? tree))) "OK — structurally valid" "Parse error") + (= t "get-context") + (let ((ctx (get-context (first tree) (list 2)))) + (if (empty? ctx) "Path (0 2) not found" + (join "\n" ctx))) + (= t "serialize") + (join "\n\n" (map sx-serialize tree)) + (= t "hypersx") + (sx->hypersx tree) + :else "Select a tool"))))))) + + (run-tool) + + (div :class "space-y-3" + (div :class "space-y-2" + (label :class "text-sm font-medium text-stone-700" "Source") + (textarea :class "w-full font-mono text-xs bg-stone-50 border border-stone-300 rounded p-2" + :rows 6 :bind source)) + (div :class "flex gap-2 flex-wrap items-center" + (for-each (fn (t) + (button + :class (str "px-3 py-1 text-xs rounded border transition " + (if (= (deref tool) t) + "bg-violet-600 text-white border-violet-600" + "bg-white text-stone-600 border-stone-300 hover:border-violet-400")) + :on-click (fn (e) (reset! tool t) (run-tool)) + t)) + (list "summarise" "read-tree" "find-all" "validate" "get-context" "serialize" "hypersx")) + (button :class "px-3 py-1 text-xs rounded bg-stone-700 text-white" + :on-click (fn (e) (run-tool)) + "Run")) + (pre :class "font-mono text-xs text-stone-700 bg-stone-50 border border-stone-200 rounded p-3 whitespace-pre-wrap overflow-x-auto max-h-64 overflow-y-auto" + (deref output)))))) diff --git a/sx/sx/sx-tools.sx b/sx/sx/sx-tools.sx index 0d1b63fe..a639839e 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 "Tool catalogue" :id "catalogue" (p "The SX tree MCP server provides 35+ tools across 8 categories. Every tool operates on the parsed tree, not raw text.") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Comprehension (7 tools)") (p "Read-only tools for understanding structure before editing.") (table :class "min-w-full text-sm mb-6" (thead (tr (th :class "text-left pr-4 pb-2 font-semibold text-stone-700" "Tool") (th :class "text-left pb-2 font-semibold text-stone-700" "Purpose"))) (tbody :class "text-stone-600" (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_read_tree") (td :class "py-1" "Annotated tree with path labels. Auto-summarises large files. Params: focus (expand matching subtrees), max_depth, max_lines/offset.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_summarise") (td :class "py-1" "Folded overview at configurable depth — shape without detail.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_read_subtree") (td :class "py-1" "Expand a specific subtree by path.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_get_context") (td :class "py-1" "Enclosing chain from root to a target node.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_find_all") (td :class "py-1" "Search by pattern in one file, returns matching paths.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_get_siblings") (td :class "py-1" "Siblings of a node with the target marked.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_validate") (td :class "py-1" "Structural integrity: balanced parens, valid paths.")))) (~docs/code :src ";; Focus mode — expand only matching subtrees\nsx_read_tree file=\"home.sx\" focus=\"defisland\"\n\n;; Paginated — first 30 lines\nsx_read_tree file=\"home.sx\" max_lines=30\n\n;; Context chain for a deep node\nsx_get_context file=\"home.sx\" path=\"(0 2 2 1 12)\"") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Path-based editing (4 tools)") (p "Structural edits by tree path. Fragment is parsed before the file is touched.") (table :class "min-w-full text-sm mb-6" (thead (tr (th :class "text-left pr-4 pb-2 font-semibold text-stone-700" "Tool") (th :class "text-left pb-2 font-semibold text-stone-700" "Purpose"))) (tbody :class "text-stone-600" (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_replace_node") (td :class "py-1" "Replace node at path with new parsed source.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_insert_child") (td :class "py-1" "Insert child at index within a list.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_delete_node") (td :class "py-1" "Remove node — siblings shift to fill gap.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_wrap_node") (td :class "py-1" "Wrap node in template with _ placeholder.")))) (~docs/code :src ";; Replace a node\nsx_replace_node file=\"home.sx\" path=\"(0 2 1)\"\n new_source=\"(div :class \\\"updated\\\")\"\n\n;; Wrap in a when guard\nsx_wrap_node file=\"home.sx\" path=\"(0 3)\"\n template=\"(when active _)\"") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Smart editing (4 tools)") (p "Pattern-based: find by name, then edit. No path arithmetic needed.") (table :class "min-w-full text-sm mb-6" (thead (tr (th :class "text-left pr-4 pb-2 font-semibold text-stone-700" "Tool") (th :class "text-left pb-2 font-semibold text-stone-700" "Purpose"))) (tbody :class "text-stone-600" (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_rename_symbol") (td :class "py-1" "Rename all occurrences of a symbol in a file.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_replace_by_pattern") (td :class "py-1" "Find + replace first/all nodes matching a pattern.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_insert_near") (td :class "py-1" "Insert before/after a pattern match (top-level).")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_rename_across") (td :class "py-1" "Rename symbol across all .sx files in a directory. dry_run=true to preview.")))) (~docs/code :src ";; Rename a component everywhere\nsx_rename_across dir=\"sx/\" old_name=\"~card\"\n new_name=\"~ui/card\" dry_run=true\n;; → home.sx: 3 occurrences (dry run)\n;; → layout.sx: 1 occurrence (dry run)") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Project-wide search (3 tools)") (p "Search across all .sx files in a directory tree.") (table :class "min-w-full text-sm mb-6" (thead (tr (th :class "text-left pr-4 pb-2 font-semibold text-stone-700" "Tool") (th :class "text-left pb-2 font-semibold text-stone-700" "Purpose"))) (tbody :class "text-stone-600" (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_find_across") (td :class "py-1" "Search pattern across all .sx files. Returns file + path + summary.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_comp_list") (td :class "py-1" "List all definitions (defcomp/defisland/defmacro/defpage/define) across files.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_comp_usage") (td :class "py-1" "Find all uses of a component/symbol across files.")))) (~docs/code :src ";; Who uses ~docs/section?\nsx_comp_usage dir=\"sx/\" name=\"~docs/section\"\n;; → sx-tools.sx [0,3,4] (~docs/section :title ...)\n;; → sx-tools.sx [0,3,5] (~docs/section :title ...)") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Development (7 tools)") (p "Build, test, lint, format, evaluate.") (table :class "min-w-full text-sm mb-6" (thead (tr (th :class "text-left pr-4 pb-2 font-semibold text-stone-700" "Tool") (th :class "text-left pb-2 font-semibold text-stone-700" "Purpose"))) (tbody :class "text-stone-600" (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_pretty_print") (td :class "py-1" "Reformat .sx file with indentation. All edit tools auto-format.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_write_file") (td :class "py-1" "Create/overwrite .sx file with parse validation.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_build") (td :class "py-1" "Build JS bundle (target=js) or OCaml binary (target=ocaml).")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_test") (td :class "py-1" "Run test suite — host=js or ocaml, full=true for extensions.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_format_check") (td :class "py-1" "Lint: empty bindings, missing bodies, duplicate params.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_macroexpand") (td :class "py-1" "Evaluate expression with a file's macro definitions loaded.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_eval") (td :class "py-1" "REPL — evaluate SX expressions in the MCP server env.")))) (~docs/code :src ";; Build and test in one go\nsx_build target=\"ocaml\"\nsx_test host=\"ocaml\"\n;; → Results: 1116 passed, 0 failed\n\n;; Quick REPL check\nsx_eval \"(+ (* 3 4) 5)\" ;; → 17") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Git integration (3 tools)") (p "Structural awareness of version control.") (table :class "min-w-full text-sm mb-6" (thead (tr (th :class "text-left pr-4 pb-2 font-semibold text-stone-700" "Tool") (th :class "text-left pb-2 font-semibold text-stone-700" "Purpose"))) (tbody :class "text-stone-600" (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_changed") (td :class "py-1" "List .sx files changed since a ref with structural summaries.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_diff_branch") (td :class "py-1" "Structural diff of all .sx changes on branch vs base ref.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_blame") (td :class "py-1" "Git blame for .sx file, optionally focused on a tree path.")))) (~docs/code :src ";; What changed on this branch?\nsx_diff_branch\n;; → adapter-dom.sx: ADDED [3] (define *memo-cache* ...)\n;; → adapter-dom.sx: ADDED [4] (define contains-deref? ...)\n;; → boot.sx: CHANGED [12,4,2,2,2,2,3,3,1,0] cek-try wrapper") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Test harness (1 tool)") (p "Sandboxed evaluation with mock IO.") (table :class "min-w-full text-sm mb-6" (thead (tr (th :class "text-left pr-4 pb-2 font-semibold text-stone-700" "Tool") (th :class "text-left pb-2 font-semibold text-stone-700" "Purpose"))) (tbody :class "text-stone-600" (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_harness_eval") (td :class "py-1" "Evaluate SX with mock IO platform. Returns result + IO trace.")))) (~docs/code :src ";; Test a component's IO behavior\nsx_harness_eval\n expr=\"(fetch-data \\\"users\\\" \\\"all\\\")\"\n mock=\"{:fetch (fn (url opts) {:status 200 :body \\\"[]\\\"})}\"") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Analysis (3 tools)") (p "Structural diff, documentation generation, browser testing.") (table :class "min-w-full text-sm mb-6" (thead (tr (th :class "text-left pr-4 pb-2 font-semibold text-stone-700" "Tool") (th :class "text-left pb-2 font-semibold text-stone-700" "Purpose"))) (tbody :class "text-stone-600" (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_diff") (td :class "py-1" "Structural diff between two .sx files (ADDED/REMOVED/CHANGED).")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_doc_gen") (td :class "py-1" "Generate component docs from signatures across a directory.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_playwright") (td :class "py-1" "Run Playwright browser tests for the SX docs site.")))) (~docs/code :src ";; Run all browser tests\nsx_playwright\n;; → 23 passed, 0 failed\n\n;; Generate docs for all components\nsx_doc_gen dir=\"sx/sx/\"\n;; → TYPE NAME FILE PARAMS\n;; → defcomp ~docs/page docs.sx &key title\n;; → defisland ~home/stepper home.sx ()")) (~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 "Tool catalogue" :id "catalogue" (p "The SX tree MCP server provides 35+ tools across 8 categories. Every tool operates on the parsed tree, not raw text.") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Comprehension (7 tools)") (p "Read-only tools for understanding structure before editing.") (table :class "min-w-full text-sm mb-6" (thead (tr (th :class "text-left pr-4 pb-2 font-semibold text-stone-700" "Tool") (th :class "text-left pb-2 font-semibold text-stone-700" "Purpose"))) (tbody :class "text-stone-600" (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_read_tree") (td :class "py-1" "Annotated tree with path labels. Auto-summarises large files. Params: focus (expand matching subtrees), max_depth, max_lines/offset.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_summarise") (td :class "py-1" "Folded overview at configurable depth — shape without detail.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_read_subtree") (td :class "py-1" "Expand a specific subtree by path.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_get_context") (td :class "py-1" "Enclosing chain from root to a target node.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_find_all") (td :class "py-1" "Search by pattern in one file, returns matching paths.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_get_siblings") (td :class "py-1" "Siblings of a node with the target marked.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_validate") (td :class "py-1" "Structural integrity: balanced parens, valid paths.")))) (~docs/code :src ";; Focus mode — expand only matching subtrees\nsx_read_tree file=\"home.sx\" focus=\"defisland\"\n\n;; Paginated — first 30 lines\nsx_read_tree file=\"home.sx\" max_lines=30\n\n;; Context chain for a deep node\nsx_get_context file=\"home.sx\" path=\"(0 2 2 1 12)\"") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Path-based editing (4 tools)") (p "Structural edits by tree path. Fragment is parsed before the file is touched.") (table :class "min-w-full text-sm mb-6" (thead (tr (th :class "text-left pr-4 pb-2 font-semibold text-stone-700" "Tool") (th :class "text-left pb-2 font-semibold text-stone-700" "Purpose"))) (tbody :class "text-stone-600" (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_replace_node") (td :class "py-1" "Replace node at path with new parsed source.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_insert_child") (td :class "py-1" "Insert child at index within a list.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_delete_node") (td :class "py-1" "Remove node — siblings shift to fill gap.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_wrap_node") (td :class "py-1" "Wrap node in template with _ placeholder.")))) (~docs/code :src ";; Replace a node\nsx_replace_node file=\"home.sx\" path=\"(0 2 1)\"\n new_source=\"(div :class \\\"updated\\\")\"\n\n;; Wrap in a when guard\nsx_wrap_node file=\"home.sx\" path=\"(0 3)\"\n template=\"(when active _)\"") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Smart editing (4 tools)") (p "Pattern-based: find by name, then edit. No path arithmetic needed.") (table :class "min-w-full text-sm mb-6" (thead (tr (th :class "text-left pr-4 pb-2 font-semibold text-stone-700" "Tool") (th :class "text-left pb-2 font-semibold text-stone-700" "Purpose"))) (tbody :class "text-stone-600" (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_rename_symbol") (td :class "py-1" "Rename all occurrences of a symbol in a file.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_replace_by_pattern") (td :class "py-1" "Find + replace first/all nodes matching a pattern.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_insert_near") (td :class "py-1" "Insert before/after a pattern match (top-level).")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_rename_across") (td :class "py-1" "Rename symbol across all .sx files in a directory. dry_run=true to preview.")))) (~docs/code :src ";; Rename a component everywhere\nsx_rename_across dir=\"sx/\" old_name=\"~card\"\n new_name=\"~ui/card\" dry_run=true\n;; → home.sx: 3 occurrences (dry run)\n;; → layout.sx: 1 occurrence (dry run)") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Project-wide search (3 tools)") (p "Search across all .sx files in a directory tree.") (table :class "min-w-full text-sm mb-6" (thead (tr (th :class "text-left pr-4 pb-2 font-semibold text-stone-700" "Tool") (th :class "text-left pb-2 font-semibold text-stone-700" "Purpose"))) (tbody :class "text-stone-600" (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_find_across") (td :class "py-1" "Search pattern across all .sx files. Returns file + path + summary.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_comp_list") (td :class "py-1" "List all definitions (defcomp/defisland/defmacro/defpage/define) across files.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_comp_usage") (td :class "py-1" "Find all uses of a component/symbol across files.")))) (~docs/code :src ";; Who uses ~docs/section?\nsx_comp_usage dir=\"sx/\" name=\"~docs/section\"\n;; → sx-tools.sx [0,3,4] (~docs/section :title ...)\n;; → sx-tools.sx [0,3,5] (~docs/section :title ...)") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Development (7 tools)") (p "Build, test, lint, format, evaluate.") (table :class "min-w-full text-sm mb-6" (thead (tr (th :class "text-left pr-4 pb-2 font-semibold text-stone-700" "Tool") (th :class "text-left pb-2 font-semibold text-stone-700" "Purpose"))) (tbody :class "text-stone-600" (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_pretty_print") (td :class "py-1" "Reformat .sx file with indentation. All edit tools auto-format.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_write_file") (td :class "py-1" "Create/overwrite .sx file with parse validation.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_build") (td :class "py-1" "Build JS bundle (target=js) or OCaml binary (target=ocaml).")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_test") (td :class "py-1" "Run test suite — host=js or ocaml, full=true for extensions.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_format_check") (td :class "py-1" "Lint: empty bindings, missing bodies, duplicate params.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_macroexpand") (td :class "py-1" "Evaluate expression with a file's macro definitions loaded.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_eval") (td :class "py-1" "REPL — evaluate SX expressions in the MCP server env.")))) (~docs/code :src ";; Build and test in one go\nsx_build target=\"ocaml\"\nsx_test host=\"ocaml\"\n;; → Results: 1116 passed, 0 failed\n\n;; Quick REPL check\nsx_eval \"(+ (* 3 4) 5)\" ;; → 17") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Git integration (3 tools)") (p "Structural awareness of version control.") (table :class "min-w-full text-sm mb-6" (thead (tr (th :class "text-left pr-4 pb-2 font-semibold text-stone-700" "Tool") (th :class "text-left pb-2 font-semibold text-stone-700" "Purpose"))) (tbody :class "text-stone-600" (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_changed") (td :class "py-1" "List .sx files changed since a ref with structural summaries.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_diff_branch") (td :class "py-1" "Structural diff of all .sx changes on branch vs base ref.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_blame") (td :class "py-1" "Git blame for .sx file, optionally focused on a tree path.")))) (~docs/code :src ";; What changed on this branch?\nsx_diff_branch\n;; → adapter-dom.sx: ADDED [3] (define *memo-cache* ...)\n;; → adapter-dom.sx: ADDED [4] (define contains-deref? ...)\n;; → boot.sx: CHANGED [12,4,2,2,2,2,3,3,1,0] cek-try wrapper") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Test harness (1 tool)") (p "Sandboxed evaluation with mock IO.") (table :class "min-w-full text-sm mb-6" (thead (tr (th :class "text-left pr-4 pb-2 font-semibold text-stone-700" "Tool") (th :class "text-left pb-2 font-semibold text-stone-700" "Purpose"))) (tbody :class "text-stone-600" (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_harness_eval") (td :class "py-1" "Evaluate SX with mock IO platform. Returns result + IO trace.")))) (~docs/code :src ";; Test a component's IO behavior\nsx_harness_eval\n expr=\"(fetch-data \\\"users\\\" \\\"all\\\")\"\n mock=\"{:fetch (fn (url opts) {:status 200 :body \\\"[]\\\"})}\"") (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Analysis (3 tools)") (p "Structural diff, documentation generation, browser testing.") (table :class "min-w-full text-sm mb-6" (thead (tr (th :class "text-left pr-4 pb-2 font-semibold text-stone-700" "Tool") (th :class "text-left pb-2 font-semibold text-stone-700" "Purpose"))) (tbody :class "text-stone-600" (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_diff") (td :class "py-1" "Structural diff between two .sx files (ADDED/REMOVED/CHANGED).")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_doc_gen") (td :class "py-1" "Generate component docs from signatures across a directory.")) (tr (td :class "pr-4 py-1 font-mono text-xs" "sx_playwright") (td :class "py-1" "Run Playwright browser tests for the SX docs site.")))) (~docs/code :src ";; Run all browser tests\nsx_playwright\n;; → 23 passed, 0 failed\n\n;; Generate docs for all components\nsx_doc_gen dir=\"sx/sx/\"\n;; → TYPE NAME FILE PARAMS\n;; → defcomp ~docs/page docs.sx &key title\n;; → defisland ~home/stepper home.sx ()")) (~docs/section :title "Try the tools" :id "playground" (p "Paste SX source, pick a tool, see the result. These run in your browser using the same SX functions the MCP server uses.") (~sx-tools/tool-playground)) (~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."))))