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"