Add 5 MCP tools, refactor nav-data, fix deep path bug, fix Playwright failures

Nav refactoring:
- Split nav-data.sx (32 forms) into 6 files: nav-geography, nav-language,
  nav-applications, nav-etc, nav-tools, nav-tree
- Add Tools top-level nav category with SX Tools and Services pages
- New services-tools.sx page documenting the rose-ash-services MCP server

JS build fixes (fixes 5 Playwright failures):
- Wire web/web-signals.sx into JS build (stores, events, resources)
- Add cek-try primitive to JS platform (island hydration error handling)
- Merge PRIMITIVES into getRenderEnv (island env was missing primitives)
- Rename web/signals.sx → web/web-signals.sx to avoid spec/ collision

New MCP tools:
- sx_trace: step-through CEK evaluation showing lookups, calls, returns
- sx_deps: dependency analysis — free symbols + cross-file resolution
- sx_build_manifest: show build contents for JS and OCaml targets
- sx_harness_eval extended: multi-file loading + setup expressions

Deep path bug fix:
- Native OCaml list-replace and navigate bypass CEK callback chain
- Fixes sx_replace_node and sx_read_subtree corruption on paths 6+ deep

Tests: 1478/1478 JS full suite, 91/91 Playwright

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 12:09:22 +00:00
parent 4e88b8a9dd
commit 5ed984e7e3
18 changed files with 3620 additions and 112 deletions

View File

@@ -99,6 +99,8 @@ def compile_ref_to_js(
spec_mod_set.add(sm)
if "dom" in adapter_set and "signals" in SPEC_MODULES:
spec_mod_set.add("signals")
if "signals-web" in SPEC_MODULES:
spec_mod_set.add("signals-web")
if "boot" in adapter_set:
spec_mod_set.add("router")
spec_mod_set.add("deps")

View File

@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""Output JS build manifest as structured text for the MCP server."""
from __future__ import annotations
import json
import os
import re
import sys
_HERE = os.path.dirname(os.path.abspath(__file__))
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", ".."))
if _PROJECT not in sys.path:
sys.path.insert(0, _PROJECT)
from hosts.javascript.platform import (
ADAPTER_FILES, ADAPTER_DEPS, SPEC_MODULES, SPEC_MODULE_ORDER,
PRIMITIVES_JS_MODULES, _ALL_JS_MODULES, EXTENSION_NAMES,
)
def extract_primitives(js_code: str) -> list[str]:
"""Extract PRIMITIVES["name"] registrations from JS code."""
return sorted(set(re.findall(r'PRIMITIVES\["([^"]+)"\]', js_code)))
def main():
# Core spec files (always included)
core_files = [
"evaluator.sx (frames + eval + CEK)",
"freeze.sx (serializable state)",
"content.sx (content-addressed computation)",
"render.sx (core renderer)",
"web-forms.sx (defstyle, deftype, defeffect)",
]
# Adapters
adapter_lines = []
for name, (filename, label) in sorted(ADAPTER_FILES.items()):
deps = ADAPTER_DEPS.get(name, [])
dep_str = f" (deps: {', '.join(deps)})" if deps else ""
adapter_lines.append(f" {name:18s} {filename:22s} {label}{dep_str}")
# Spec modules
module_lines = []
for name in SPEC_MODULE_ORDER:
if name in SPEC_MODULES:
filename, label = SPEC_MODULES[name]
module_lines.append(f" {name:18s} {filename:22s} {label}")
# Extensions
ext_lines = [f" {name}" for name in sorted(EXTENSION_NAMES)]
# Primitive modules
prim_lines = []
for mod_name in sorted(_ALL_JS_MODULES):
if mod_name in PRIMITIVES_JS_MODULES:
prims = extract_primitives(PRIMITIVES_JS_MODULES[mod_name])
prim_lines.append(f" {mod_name} ({len(prims)}): {', '.join(prims)}")
# Current build file
build_path = os.path.join(_PROJECT, "shared", "static", "scripts", "sx-browser.js")
build_info = ""
if os.path.exists(build_path):
size = os.path.getsize(build_path)
mtime = os.path.getmtime(build_path)
from datetime import datetime
ts = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
# Count PRIMITIVES in actual build
with open(build_path) as f:
content = f.read()
actual_prims = extract_primitives(content)
build_info = f"\nCurrent build: {size:,} bytes, {ts}, {len(actual_prims)} primitives registered"
print(f"""JS Build Manifest
=================
{build_info}
Core files (always included):
{chr(10).join(' ' + f for f in core_files)}
Adapters ({len(ADAPTER_FILES)}):
{chr(10).join(adapter_lines)}
Spec modules ({len(SPEC_MODULES)}, order: {''.join(SPEC_MODULE_ORDER)}):
{chr(10).join(module_lines)}
Extensions ({len(EXTENSION_NAMES)}):
{chr(10).join(ext_lines)}
Primitive modules ({len(_ALL_JS_MODULES)}):
{chr(10).join(prim_lines)}""")
if __name__ == "__main__":
main()

View File

@@ -61,6 +61,7 @@ SPEC_MODULES = {
"deps": ("deps.sx", "deps (component dependency analysis)"),
"router": ("router.sx", "router (client-side route matching)"),
"signals": ("signals.sx", "signals (reactive signal runtime)"),
"signals-web": ("web-signals.sx", "signals-web (stores, events, resources)"),
"page-helpers": ("page-helpers.sx", "page-helpers (pure data transformation helpers)"),
"types": ("types.sx", "types (gradual type system)"),
"vm": ("vm.sx", "vm (bytecode virtual machine)"),
@@ -68,7 +69,7 @@ SPEC_MODULES = {
# Note: frames and cek are now part of evaluator.sx (always loaded as core)
# Explicit ordering for spec modules with dependencies.
SPEC_MODULE_ORDER = ["deps", "page-helpers", "router", "signals", "types", "vm"]
SPEC_MODULE_ORDER = ["deps", "page-helpers", "router", "signals", "signals-web", "types", "vm"]
EXTENSION_NAMES = {"continuations"}
@@ -2665,6 +2666,17 @@ PLATFORM_ORCHESTRATION_JS = """
: catchFn;
try { return t(); } catch (e) { return c(e); }
}
function cekTry(thunkFn, handlerFn) {
try {
var result = _wrapSxFn(thunkFn)();
if (!handlerFn || handlerFn === NIL) return [SYM("ok"), result];
return result;
} catch (e) {
var msg = (e && e.message) ? e.message : String(e);
if (handlerFn && handlerFn !== NIL) return _wrapSxFn(handlerFn)(msg);
return [SYM("error"), msg];
}
}
function errorMessage(e) {
return e && e.message ? e.message : String(e);
}
@@ -3077,7 +3089,7 @@ PLATFORM_BOOT_JS = """
}
function getRenderEnv(extraEnv) {
return extraEnv ? merge(componentEnv, extraEnv) : componentEnv;
return extraEnv ? merge(componentEnv, PRIMITIVES, extraEnv) : merge(componentEnv, PRIMITIVES);
}
function mergeEnvs(base, newEnv) {
@@ -3287,7 +3299,18 @@ def fixups_js(has_html, has_sx, has_dom, has_signals=False, has_deps=False, has_
try { localStorage.removeItem(key); } catch (e) {}
return NIL;
};
if (typeof sxParse === "function") PRIMITIVES["sx-parse"] = sxParse;''']
if (typeof sxParse === "function") PRIMITIVES["sx-parse"] = sxParse;
PRIMITIVES["cek-try"] = function(thunkFn, handlerFn) {
try {
var result = _wrapSxFn(thunkFn)();
if (!handlerFn || handlerFn === NIL) return [SYM("ok"), result];
return result;
} catch (e) {
var msg = (e && e.message) ? e.message : String(e);
if (handlerFn && handlerFn !== NIL) return _wrapSxFn(handlerFn)(msg);
return [SYM("error"), msg];
}
};''']
if has_deps:
lines.append('''
// Platform deps functions (native JS, not transpiled — need explicit registration)

View File

@@ -506,6 +506,7 @@
"to-kebab" "toKebab"
"log-info" "logInfo"
"log-warn" "logWarn"
"cek-try" "cekTry"
"log-parse-error" "logParseError"
"_page-routes" "_pageRoutes"
"process-page-scripts" "processPageScripts"

View File

@@ -177,6 +177,27 @@ let setup_env () =
| [f; List l] | [f; ListRef { contents = l }] ->
List (List.mapi (fun i x -> Sx_ref.cek_call f (List [Number (float_of_int i); x])) l)
| _ -> List []);
(* Native list-replace — bypasses CEK map-indexed callback chain for deep tree edits *)
bind "list-replace" (fun args -> match args with
| [List l; Number idx; v] ->
let i = int_of_float idx in
List (List.mapi (fun j x -> if j = i then v else x) l)
| [ListRef { contents = l }; Number idx; v] ->
let i = int_of_float idx in
List (List.mapi (fun j x -> if j = i then v else x) l)
| _ -> Nil);
(* Native navigate — bypasses CEK reduce callback chain for deep path reads *)
bind "navigate" (fun args -> match args with
| [tree; List path] | [tree; ListRef { contents = path }] ->
let nodes = match tree with List _ | ListRef _ -> tree | _ -> List [tree] in
List.fold_left (fun current idx ->
match current, idx with
| (List l | ListRef { contents = l }), Number n ->
let i = int_of_float n in
if i >= 0 && i < List.length l then List.nth l i else Nil
| _ -> Nil
) nodes path
| _ -> Nil);
bind "trim" (fun args -> match args with
| [String s] -> String (String.trim s) | _ -> String "");
bind "split" (fun args -> match args with
@@ -683,38 +704,93 @@ let rec handle_tool name args =
Filename.dirname spec_dir
in
let spec = args |> member "spec" |> to_string_option in
let spec_arg = match spec with Some s -> " " ^ s | None -> "" in
let cmd = Printf.sprintf "cd %s/tests/playwright && npx playwright test%s --reporter=line 2>&1" project_dir spec_arg in
let ic = Unix.open_process_in cmd in
let lines = ref [] in
(try while true do lines := input_line ic :: !lines done with End_of_file -> ());
ignore (Unix.close_process_in ic);
let all_lines = List.rev !lines in
let fails = List.filter (fun l -> let t = String.trim l in
String.length t > 1 && (t.[0] = '\xE2' (**) || (String.length t > 4 && String.sub t 0 4 = "FAIL"))) all_lines in
let summary = List.find_opt (fun l ->
try let _ = Str.search_forward (Str.regexp "passed\\|failed") l 0 in true
with Not_found -> false) (List.rev all_lines) in
let result = match summary with
| Some s ->
if fails = [] then s
else s ^ "\n\nFailures:\n" ^ String.concat "\n" fails
| None ->
let last_n = List.filteri (fun i _ -> i >= List.length all_lines - 10) all_lines in
String.concat "\n" last_n
let mode = args |> member "mode" |> to_string_option in
let url = args |> member "url" |> to_string_option in
let selector = args |> member "selector" |> to_string_option in
let expr = args |> member "expr" |> to_string_option in
let actions = args |> member "actions" |> to_string_option in
(* Determine whether to run specs or the inspector *)
let use_inspector = match mode with
| Some m when m <> "run" -> true
| _ -> spec = None && mode <> None
in
text_result result
if not use_inspector then begin
(* Original spec runner *)
let spec_arg = match spec with Some s -> " " ^ s | None -> "" in
let cmd = Printf.sprintf "cd %s/tests/playwright && npx playwright test%s --reporter=line 2>&1" project_dir spec_arg in
let ic = Unix.open_process_in cmd in
let lines = ref [] in
(try while true do lines := input_line ic :: !lines done with End_of_file -> ());
ignore (Unix.close_process_in ic);
let all_lines = List.rev !lines in
let fails = List.filter (fun l -> let t = String.trim l in
String.length t > 1 && (t.[0] = '\xE2' (**) || (String.length t > 4 && String.sub t 0 4 = "FAIL"))) all_lines in
let summary = List.find_opt (fun l ->
try let _ = Str.search_forward (Str.regexp "passed\\|failed") l 0 in true
with Not_found -> false) (List.rev all_lines) in
let result = match summary with
| Some s ->
if fails = [] then s
else s ^ "\n\nFailures:\n" ^ String.concat "\n" fails
| None ->
let last_n = List.filteri (fun i _ -> i >= List.length all_lines - 10) all_lines in
String.concat "\n" last_n
in
text_result result
end else begin
(* SX-aware inspector *)
let inspector_args = `Assoc (List.filter_map Fun.id [
(match mode with Some m -> Some ("mode", `String m) | None -> Some ("mode", `String "inspect"));
(match url with Some u -> Some ("url", `String u) | None -> None);
(match selector with Some s -> Some ("selector", `String s) | None -> None);
(match expr with Some e -> Some ("expr", `String e) | None -> None);
(match actions with Some a -> Some ("actions", `String a) | None -> None);
]) in
let args_json = Yojson.Safe.to_string (Yojson.Safe.from_string (Yojson.Basic.to_string inspector_args)) in
let cmd = Printf.sprintf "cd %s && node tests/playwright/sx-inspect.js '%s' 2>&1" project_dir (String.escaped args_json) in
let ic = Unix.open_process_in cmd in
let lines = ref [] in
(try while true do lines := input_line ic :: !lines done with End_of_file -> ());
ignore (Unix.close_process_in ic);
let raw = String.concat "\n" (List.rev !lines) in
(* Try to parse as JSON and format nicely *)
try
let json = Yojson.Basic.from_string raw in
let pretty = Yojson.Basic.pretty_to_string json in
text_result pretty
with _ ->
text_result raw
end
| "sx_harness_eval" ->
let expr_str = args |> member "expr" |> to_string in
let mock_str = args |> member "mock" |> to_string_option in
let file = args |> member "file" |> to_string_option in
let setup_str = args |> member "setup" |> to_string_option in
let files_json = try args |> member "files" with _ -> `Null in
let e = !env in
(* Optionally load a file's definitions *)
(match file with
| Some f ->
(try load_sx_file e f
with exn -> Printf.eprintf "[mcp] Warning: %s: %s\n%!" f (Printexc.to_string exn))
let warnings = ref [] in
(* Collect all files to load *)
let all_files = match files_json with
| `List items ->
List.map (fun j -> Yojson.Safe.Util.to_string j) items
| _ -> match file with Some f -> [f] | None -> []
in
(* Load each file *)
List.iter (fun f ->
try load_sx_file e f
with exn ->
warnings := Printf.sprintf "Warning: %s: %s" f (Printexc.to_string exn) :: !warnings
) all_files;
(* Run setup expression if provided *)
(match setup_str with
| Some s ->
let setup_exprs = Sx_parser.parse_all s in
List.iter (fun expr ->
try ignore (Sx_ref.eval_expr expr (Env e))
with exn ->
warnings := Printf.sprintf "Setup error: %s" (Printexc.to_string exn) :: !warnings
) setup_exprs
| None -> ());
(* Create harness with optional mock overrides *)
let mock_arg = match mock_str with
@@ -743,7 +819,10 @@ let rec handle_tool name args =
) items)
| _ -> "\n\n(no IO calls)"
in
text_result (Printf.sprintf "Result: %s%s" (Sx_types.inspect result) log_str)
let warn_str = if !warnings = [] then "" else
"\n\nWarnings:\n" ^ String.concat "\n" (List.rev !warnings)
in
text_result (Printf.sprintf "Result: %s%s%s" (Sx_types.inspect result) log_str warn_str)
| "sx_write_file" ->
let file = args |> member "file" |> to_string in
@@ -912,6 +991,219 @@ let rec handle_tool name args =
) Nil exprs in
text_result (Sx_runtime.value_to_str result)
| "sx_trace" ->
let expr_str = args |> member "expr" |> to_string in
let max_steps = (try args |> member "max_steps" |> to_int with _ -> 200) in
let file = try Some (args |> member "file" |> to_string) with _ -> None in
let e = !env in
(match file with
| Some f -> (try load_sx_file e f with _ -> ())
| None -> ());
let exprs = Sx_parser.parse_all expr_str in
let expr = match exprs with [e] -> e | _ -> List exprs in
let state = ref (Sx_ref.make_cek_state expr (Env e) (List [])) in
let steps = Buffer.create 2048 in
let step_count = ref 0 in
let truncate s n = if String.length s > n then String.sub s 0 n ^ "..." else s in
(try
while !step_count < max_steps do
let s = !state in
(match s with
| CekState cs ->
incr step_count;
let n = !step_count in
if cs.cs_phase = "eval" then begin
let ctrl = cs.cs_control in
(match ctrl with
| Symbol sym_name ->
let resolved = (try
let v = Sx_ref.eval_expr ctrl cs.cs_env in
truncate (Sx_runtime.value_to_str v) 60
with _ -> "???") in
Buffer.add_string steps
(Printf.sprintf "%3d LOOKUP %s → %s\n" n sym_name resolved)
| List (hd :: _) ->
let head_str = truncate (Sx_runtime.value_to_str hd) 30 in
let ctrl_str = truncate (Sx_runtime.value_to_str ctrl) 80 in
Buffer.add_string steps
(Printf.sprintf "%3d CALL %s\n" n ctrl_str);
ignore head_str
| _ ->
Buffer.add_string steps
(Printf.sprintf "%3d LITERAL %s\n" n
(truncate (Sx_runtime.value_to_str ctrl) 60)))
end else begin
(* continue phase *)
let val_str = truncate (Sx_runtime.value_to_str cs.cs_value) 60 in
let kont = cs.cs_kont in
let frame_type = match kont with
| List (Dict d :: _) ->
(match Hashtbl.find_opt d "type" with
| Some (String s) -> s | _ -> "?")
| List (CekState ks :: _) ->
(match ks.cs_control with
| Dict d ->
(match Hashtbl.find_opt d "type" with
| Some (String s) -> s | _ -> "?")
| _ -> "?")
| _ -> "done" in
Buffer.add_string steps
(Printf.sprintf "%3d RETURN %s → %s\n" n val_str frame_type)
end;
(match Sx_ref.cek_terminal_p s with
| Bool true -> raise Exit
| _ -> ());
state := Sx_ref.cek_step s
| _ -> raise Exit)
done
with
| Exit -> ()
| Eval_error msg ->
Buffer.add_string steps (Printf.sprintf "ERROR: %s\n" msg)
| exn ->
Buffer.add_string steps (Printf.sprintf "ERROR: %s\n" (Printexc.to_string exn)));
let final_val = (match !state with
| CekState cs -> Sx_runtime.value_to_str cs.cs_value
| v -> Sx_runtime.value_to_str v) in
text_result (Printf.sprintf "Result: %s\n\nTrace (%d steps):\n%s"
final_val !step_count (Buffer.contents steps))
| "sx_deps" ->
let file = args |> member "file" |> to_string in
let name = try Some (args |> member "name" |> to_string) with _ -> None in
let dir = try args |> member "dir" |> to_string with _ ->
try Sys.getenv "SX_PROJECT_DIR" with Not_found ->
try Sys.getenv "PWD" with Not_found -> "." in
let tree = parse_file file in
(* Find the target subtree *)
let target = match name with
| Some n ->
(* Find the named define/defcomp/defisland *)
let items = match tree with List l | ListRef { contents = l } -> l | _ -> [tree] in
let found = List.find_opt (fun item ->
match item with
| List (Symbol head :: Symbol def_name :: _)
| List (Symbol head :: List (Symbol def_name :: _) :: _)
when (head = "define" || head = "defcomp" || head = "defisland" ||
head = "defmacro" || head = "deftest") ->
def_name = n || ("~" ^ def_name) = n || def_name = String.sub n 1 (String.length n - 1)
| _ -> false
) items in
(match found with Some f -> f | None -> tree)
| None -> tree
in
let free_syms = call_sx "collect-free-symbols" [target] in
let sym_names = match free_syms with
| List items | ListRef { contents = items } ->
List.filter_map (fun v -> match v with String s -> Some s | _ -> None) items
| _ -> []
in
(* Resolve where each symbol is defined *)
let file_defines = Hashtbl.create 32 in
let same_file_items = match tree with List l | ListRef { contents = l } -> l | _ -> [] in
List.iter (fun item ->
match item with
| List (Symbol head :: Symbol def_name :: _)
when (head = "define" || head = "defcomp" || head = "defisland" || head = "defmacro") ->
Hashtbl.replace file_defines def_name true
| _ -> ()
) same_file_items;
(* Check primitives *)
let is_prim name = try ignore (Sx_primitives.get_primitive name); true with _ -> false in
(* Scan directory for definitions *)
let all_sx_files = glob_sx_files dir in
let ext_defs = Hashtbl.create 64 in
List.iter (fun path ->
if path <> file then
try
let t = parse_file path in
let items = match t with List l | ListRef { contents = l } -> l | _ -> [] in
List.iter (fun item ->
match item with
| List (Symbol head :: Symbol def_name :: _)
when (head = "define" || head = "defcomp" || head = "defisland" || head = "defmacro") ->
if not (Hashtbl.mem ext_defs def_name) then
Hashtbl.replace ext_defs def_name (relative_path ~base:dir path)
| _ -> ()
) items
with _ -> ()
) all_sx_files;
(* Format output *)
let lines = List.map (fun sym ->
if Hashtbl.mem file_defines sym then
Printf.sprintf " %-30s (same file)" sym
else if is_prim sym then
Printf.sprintf " %-30s [primitive]" sym
else match Hashtbl.find_opt ext_defs sym with
| Some path -> Printf.sprintf " %-30s %s" sym path
| None -> Printf.sprintf " %-30s ???" sym
) sym_names in
let header = match name with
| Some n -> Printf.sprintf "Dependencies of %s in %s" n file
| None -> Printf.sprintf "Dependencies of %s" file
in
text_result (Printf.sprintf "%s\n%d symbols referenced:\n%s"
header (List.length sym_names) (String.concat "\n" lines))
| "sx_build_manifest" ->
let target = (try args |> member "target" |> to_string with _ -> "js") in
(match target with
| "ocaml" ->
let e = !env in
(* Collect all bindings from the env *)
let bindings = ref [] in
(* Walk env chain collecting all bindings *)
let rec collect_bindings env acc =
Hashtbl.iter (fun k v ->
if not (Hashtbl.mem acc k) then Hashtbl.replace acc k v
) env.bindings;
match env.parent with Some p -> collect_bindings p acc | None -> ()
in
let all = Hashtbl.create 256 in
collect_bindings e all;
Hashtbl.iter (fun k v ->
let kind = match v with
| NativeFn _ -> "native"
| Lambda _ -> "lambda"
| Component _ -> "component"
| Island _ -> "island"
| Macro _ -> "macro"
| _ -> "value"
in
bindings := (k, kind) :: !bindings
) all;
let sorted = List.sort (fun (a,_) (b,_) -> String.compare a b) !bindings in
let by_kind = Hashtbl.create 8 in
List.iter (fun (name, kind) ->
let cur = try Hashtbl.find by_kind kind with Not_found -> [] in
Hashtbl.replace by_kind kind (name :: cur)
) sorted;
let sections = Buffer.create 2048 in
Buffer.add_string sections "OCaml Build Manifest\n====================\n\n";
Buffer.add_string sections (Printf.sprintf "Total bindings: %d\n\n" (List.length sorted));
Buffer.add_string sections "Loaded files: parser.sx, tree-tools.sx, harness.sx\n\n";
List.iter (fun kind ->
match Hashtbl.find_opt by_kind kind with
| Some names ->
let rev_names = List.rev names in
Buffer.add_string sections
(Printf.sprintf "%s (%d):\n %s\n\n" kind (List.length rev_names)
(String.concat ", " rev_names))
| None -> ()
) ["native"; "lambda"; "macro"; "component"; "island"; "value"];
text_result (Buffer.contents sections)
| _ ->
let project_dir = try Sys.getenv "SX_PROJECT_DIR" with Not_found ->
try Sys.getenv "PWD" with Not_found -> "." in
let cmd = Printf.sprintf "cd %s && python3 hosts/javascript/manifest.py 2>&1"
(Filename.quote project_dir) in
let ic = Unix.open_process_in cmd in
let buf = Buffer.create 4096 in
(try while true do Buffer.add_string buf (input_line ic ^ "\n") done
with End_of_file -> ());
ignore (Unix.close_process_in ic);
text_result (Buffer.contents buf))
| _ -> error_result ("Unknown tool: " ^ name)
and write_edit file result =
@@ -980,6 +1272,16 @@ let tool_definitions = `List [
[file_prop; path_prop; ("wrapper", `Assoc [("type", `String "string"); ("description", `String "Wrapper with _ placeholder")])] ["file"; "path"; "wrapper"];
tool "sx_eval" "Evaluate an SX expression. Environment has parser + tree-tools + primitives."
[("expr", `Assoc [("type", `String "string"); ("description", `String "SX expression to evaluate")])] ["expr"];
tool "sx_trace" "Step-through SX evaluation showing each CEK machine step (symbol lookups, function calls, returns). Useful for debugging."
[("expr", `Assoc [("type", `String "string"); ("description", `String "SX expression to trace")]);
("file", `Assoc [("type", `String "string"); ("description", `String "Optional .sx file to load for definitions")]);
("max_steps", `Assoc [("type", `String "integer"); ("description", `String "Max CEK steps to show (default: 200)")])] ["expr"];
tool "sx_deps" "Dependency analysis for a component or file. Shows all referenced symbols and where they're defined."
[file_prop;
("name", `Assoc [("type", `String "string"); ("description", `String "Specific define/defcomp/defisland to analyze")]);
("dir", `Assoc [("type", `String "string"); ("description", `String "Directory to search for definitions (default: project root)")])] ["file"];
tool "sx_build_manifest" "Show build manifest: which modules, primitives, adapters, and exports are included in a JS or OCaml build."
[("target", `Assoc [("type", `String "string"); ("description", `String "Build target: \"js\" (default) or \"ocaml\"")])] [];
tool "sx_find_across" "Search for a pattern across all .sx files under a directory. Returns file paths, tree paths, and summaries."
[dir_prop; ("pattern", `Assoc [("type", `String "string"); ("description", `String "Search pattern")])] ["dir"; "pattern"];
tool "sx_comp_list" "List all definitions (defcomp, defisland, defmacro, defpage, define) across .sx files in a directory."
@@ -1042,13 +1344,20 @@ let tool_definitions = `List [
[file_prop; path_prop] ["file"];
tool "sx_doc_gen" "Generate component documentation from all defcomp/defisland/defmacro signatures in a directory."
[dir_prop] ["dir"];
tool "sx_harness_eval" "Evaluate SX in a test harness with mock IO. Returns result + IO trace. Use mock param to override default mock responses."
tool "sx_harness_eval" "Evaluate SX in a test harness with mock IO. Returns result + IO trace. Supports loading multiple files and setup expressions."
[("expr", `Assoc [("type", `String "string"); ("description", `String "SX expression to evaluate")]);
("mock", `Assoc [("type", `String "string"); ("description", `String "Optional mock platform overrides as SX dict, e.g. {:fetch (fn (url) {:status 200})}")]);
("file", `Assoc [("type", `String "string"); ("description", `String "Optional .sx file to load for definitions")])]
("file", `Assoc [("type", `String "string"); ("description", `String "Optional .sx file to load for definitions")]);
("files", `Assoc [("type", `String "array"); ("items", `Assoc [("type", `String "string")]); ("description", `String "Multiple .sx files to load in order")]);
("setup", `Assoc [("type", `String "string"); ("description", `String "SX setup expression to run before main evaluation")])]
["expr"];
tool "sx_playwright" "Run Playwright browser tests for the SX docs site. Optionally specify a single spec file."
[("spec", `Assoc [("type", `String "string"); ("description", `String "Optional spec file name (e.g. demo-interactions.spec.js)")])]
tool "sx_playwright" "Run Playwright browser tests or inspect SX pages interactively. Modes: run (spec files), inspect (page report), diff (SSR vs hydrated), eval (JS expression), interact (action sequence), screenshot."
[("spec", `Assoc [("type", `String "string"); ("description", `String "Spec file to run (run mode). e.g. stepper.spec.js")]);
("mode", `Assoc [("type", `String "string"); ("description", `String "Mode: run, inspect, diff, eval, interact, screenshot")]);
("url", `Assoc [("type", `String "string"); ("description", `String "URL path to navigate to (default: /)")]);
("selector", `Assoc [("type", `String "string"); ("description", `String "CSS selector to focus on (screenshot mode)")]);
("expr", `Assoc [("type", `String "string"); ("description", `String "JS expression to evaluate (eval mode)")]);
("actions", `Assoc [("type", `String "string"); ("description", `String "Semicolon-separated action sequence (interact mode). Actions: click:sel, fill:sel:val, wait:ms, text:sel, html:sel, attrs:sel, screenshot, screenshot:sel, count:sel, visible:sel")])]
[];
]