From cfde5bc4914793011099373b7c89edd3469aa603 Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 6 Mar 2026 00:06:09 +0000 Subject: [PATCH] Fix sync IO primitives unreachable from sx_ref.py evaluator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit app-url, asset-url, config, jinja-global, relations-from are declared as IO in boundary.sx but called inline in .sx code (inside let/filter). async_eval_ref.py only intercepts IO at the top level — nested calls fall through to sx_ref.eval_expr which couldn't find them. Register sync bridge wrappers directly in _PRIMITIVES (bypassing @register_primitive validation since they're boundary.sx, not primitives.sx). Both async and sync eval paths now work. Co-Authored-By: Claude Opus 4.6 --- shared/sx/primitives.py | 52 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/shared/sx/primitives.py b/shared/sx/primitives.py index 942f7b6..f9938a4 100644 --- a/shared/sx/primitives.py +++ b/shared/sx/primitives.py @@ -558,3 +558,55 @@ def prim_merge_styles(*styles: Any) -> Any: if len(valid) == 1: return valid[0] return merge_styles(valid) + + +# --------------------------------------------------------------------------- +# Sync IO bridge primitives +# +# These are declared in boundary.sx (I/O tier), NOT primitives.sx. +# They bypass @register_primitive validation because they aren't pure. +# But they must be evaluator-visible because they're called inline in .sx +# code (inside let, filter, etc.) where the async IO interceptor can't +# reach them — particularly in async_eval_ref.py which only intercepts +# IO at the top level. +# +# The async evaluators also intercept these via IO_PRIMITIVES, so the +# async path works too. This registration ensures the sync fallback works. +# --------------------------------------------------------------------------- + +def _bridge_app_url(service, *path_parts): + from shared.infrastructure.urls import app_url + path = str(path_parts[0]) if path_parts else "/" + return app_url(str(service), path) + +def _bridge_asset_url(*path_parts): + from shared.infrastructure.urls import asset_url + path = str(path_parts[0]) if path_parts else "" + return asset_url(path) + +def _bridge_config(key): + from shared.config import config + cfg = config() + return cfg.get(str(key)) + +def _bridge_jinja_global(key, *default): + from quart import current_app + d = default[0] if default else None + return current_app.jinja_env.globals.get(str(key), d) + +def _bridge_relations_from(entity_type): + from shared.sx.relations import relations_from + return [ + { + "name": d.name, "from_type": d.from_type, "to_type": d.to_type, + "cardinality": d.cardinality, "nav": d.nav, + "nav_icon": d.nav_icon, "nav_label": d.nav_label, + } + for d in relations_from(str(entity_type)) + ] + +_PRIMITIVES["app-url"] = _bridge_app_url +_PRIMITIVES["asset-url"] = _bridge_asset_url +_PRIMITIVES["config"] = _bridge_config +_PRIMITIVES["jinja-global"] = _bridge_jinja_global +_PRIMITIVES["relations-from"] = _bridge_relations_from