Files
rose-ash/shared/sx/query_executor.py
giles df461beec2 SxExpr aser wire format fix + Playwright test infrastructure + blob protocol
Aser serialization: aser-call/fragment now return SxExpr instead of String.
serialize/inspect passes SxExpr through unquoted, preventing the double-
escaping (\" → \\\" ) that broke client-side parsing when aser wire format
was output via raw! into <script> tags. Added make-sx-expr + sx-expr-source
primitives to OCaml and JS hosts.

Binary blob protocol: eval, aser, aser-slot, and sx-page-full now send SX
source as length-prefixed blobs instead of escaped strings. Eliminates pipe
desync from concurrent requests and removes all string-escape round-trips
between Python and OCaml.

Bridge safety: re-entrancy guard (_in_io_handler) raises immediately if an
IO handler tries to call the bridge, preventing silent deadlocks.

Fetch error logging: orchestration.sx error callback now logs method + URL
via log-warn. Platform catches (fetchAndRestore, fetchPreload, bindBoostForm)
also log errors instead of silently swallowing them.

Transpiler fixes: makeEnv, scopePeek, scopeEmit, makeSxExpr added as
platform function definitions + transpiler mappings — were referenced in
transpiled code but never defined as JS functions.

Playwright test infrastructure:
- nav() captures JS errors and fails fast with the actual error message
- Checks for [object Object] rendering artifacts
- New tests: delete-row interaction, full page refresh, back button,
  direct load with fresh context, code block content verification
- Default base URL changed to localhost:8013 (standalone dev server)
- docker-compose.dev-sx.yml: port 8013 exposed for local testing
- test-sx-build.sh: build + unit tests + Playwright smoke tests

Geography content: index page component written (sx/sx/geography/index.sx)
describing OCaml evaluator, wire formats, rendering pipeline, and topic
links. Wiring blocked by aser-expand-component children passing issue.

Tests: 1080/1080 JS, 952/952 OCaml, 66/66 Playwright

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 22:17:43 +00:00

111 lines
3.5 KiB
Python

"""
Execute defquery / defaction definitions.
Unlike fragment handlers (which produce SX markup via ``async_eval_to_sx``),
query/action defs produce **data** (dicts, lists, scalars) that get
JSON-serialized by the calling blueprint. Uses ``async_eval()`` with
the I/O primitive pipeline so ``(service ...)`` calls are awaited inline.
"""
from __future__ import annotations
from typing import Any
from .types import QueryDef, ActionDef, NIL
async def execute_query(query_def: QueryDef, params: dict[str, str]) -> Any:
"""Execute a defquery and return a JSON-serializable result.
Parameters are bound from request query string args.
"""
from .jinja_bridge import get_component_env, _get_request_context
import os
env = dict(get_component_env())
env.update(query_def.closure)
# Bind params from request args (try kebab-case and snake_case)
for param in query_def.params:
snake = param.replace("-", "_")
val = params.get(param, params.get(snake, NIL))
# Coerce type=int for common patterns
if isinstance(val, str) and val.lstrip("-").isdigit():
val = int(val)
env[param] = val
if os.environ.get("SX_USE_OCAML") == "1":
from .ocaml_bridge import get_bridge
from .parser import serialize, parse_all
from .pages import _wrap_with_env
bridge = await get_bridge()
sx_text = _wrap_with_env(query_def.body, env)
ctx = {"_helper_service": ""}
raw = await bridge.eval(sx_text, ctx=ctx)
if raw:
parsed = parse_all(raw)
result = parsed[0] if parsed else None
else:
result = None
return _normalize(result)
if os.environ.get("SX_USE_REF") == "1":
from .ref.async_eval_ref import async_eval
else:
from .async_eval import async_eval
ctx = _get_request_context()
result = await async_eval(query_def.body, env, ctx)
return _normalize(result)
async def execute_action(action_def: ActionDef, payload: dict[str, Any]) -> Any:
"""Execute a defaction and return a JSON-serializable result.
Parameters are bound from the JSON request body.
"""
from .jinja_bridge import get_component_env, _get_request_context
import os
env = dict(get_component_env())
env.update(action_def.closure)
# Bind params from JSON payload (try kebab-case and snake_case)
for param in action_def.params:
snake = param.replace("-", "_")
val = payload.get(param, payload.get(snake, NIL))
env[param] = val
if os.environ.get("SX_USE_OCAML") == "1":
from .ocaml_bridge import get_bridge
from .parser import serialize, parse_all
from .pages import _wrap_with_env
bridge = await get_bridge()
sx_text = _wrap_with_env(action_def.body, env)
ctx = {"_helper_service": ""}
raw = await bridge.eval(sx_text, ctx=ctx)
if raw:
parsed = parse_all(raw)
result = parsed[0] if parsed else None
else:
result = None
return _normalize(result)
if os.environ.get("SX_USE_REF") == "1":
from .ref.async_eval_ref import async_eval
else:
from .async_eval import async_eval
ctx = _get_request_context()
result = await async_eval(action_def.body, env, ctx)
return _normalize(result)
def _normalize(value: Any) -> Any:
"""Ensure result is JSON-serializable (strip NIL, convert sets, etc)."""
if value is NIL or value is None:
return None
if isinstance(value, set):
return list(value)
return value