Files
rose-ash/shared/sx/ref/BOUNDARY.md
giles 04366990ec
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m33s
Enforce SX boundary contract via boundary.sx spec + runtime validation
Add boundary.sx declaring all 34 I/O primitives, 32 page helpers, and 9
allowed boundary types. Runtime validation in boundary.py checks every
registration against the spec — undeclared primitives/helpers crash at
startup with SX_BOUNDARY_STRICT=1 (now set in both dev and prod).

Key changes:
- Move 5 I/O-in-disguise primitives (app-url, asset-url, config,
  jinja-global, relations-from) from primitives.py to primitives_io.py
- Remove duplicate url-for/route-prefix from primitives.py (already in IO)
- Fix parse-datetime to return ISO string instead of raw datetime
- Add datetime→isoformat conversion in _convert_result at the edge
- Wrap page helper return values with boundary type validation
- Replace all SxExpr(f"...") patterns with sx_call() or _sx_fragment()
- Add assert declaration to primitives.sx

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:50:02 +00:00

4.0 KiB

SX Boundary Enforcement

Principle

SX is an uninterrupted island of pure evaluation. Host code (Python, JavaScript, Rust, etc.) interacts with it only through declared entry points. The specification enforces this — violations are errors, not style suggestions.

The Three Tiers

Tier 1: Pure Primitives

Declared in primitives.sx. Stateless, synchronous, no side effects. Available in every SX environment on every target.

Examples: +, str, map, get, concat, merge

Tier 2: I/O Primitives

Declared in boundary.sx. Async, side-effectful, require host context (request, config, services). Server-side only.

Examples: frag, query, current-user, csrf-token, request-arg

Tier 3: Page Helpers

Declared in boundary.sx with a :service scope. Registered per-app, return data that .sx components render. Server-side only.

Examples: highlight (sx), editor-data (blog), all-markets-data (market)

Boundary Types

Only these types may cross the host-SX boundary:

Type Python JavaScript Rust (future)
number int, float number f64
string str string String
boolean bool boolean bool
nil NIL sentinel NIL sentinel SxValue::Nil
keyword str (colon-prefixed) string String
list list Array Vec<SxValue>
dict dict Object / Map HashMap<String, SxValue>
sx-source SxExpr wrapper string String
style-value StyleValue StyleValue StyleValue

NOT allowed: ORM models, datetime objects, request objects, raw callables, framework types. Convert at the edge before crossing.

Enforcement Mechanism

The bootstrappers (bootstrap_js.py, bootstrap_py.py, future bootstrap_rs.py, etc.) read boundary.sx and emit target-native validation:

  • Typed targets (Rust, Haskell, TypeScript): Boundary types become an enum/ADT/discriminated union. Registration functions have type signatures that reject non-SX values at compile time. You literally cannot register a primitive that returns a datetime — it won't typecheck.

  • Python + mypy: Boundary types become a Protocol/Union type. validate_boundary_value() checks at runtime; mypy catches most violations statically.

  • JavaScript: Runtime validation only. registerPrimitive() checks the name against the declared set. Boundary type checking is runtime.

The Contract

  1. Spec-first. Every primitive, I/O function, and page helper must be declared in primitives.sx or boundary.sx before it can be registered. Undeclared registration = error.

  2. SX types only. Values crossing the boundary must be SX-typed. Host-native types (datetime, ORM models, request objects) must be converted to dicts/strings at the edge.

  3. Data in, markup out. Python returns data (dicts, lists, strings). .sx files compose markup. No SX source construction in Python — no f-strings, no string concatenation, no SxExpr(f"...").

  4. Closed island. SX code can only call symbols in its env + declared primitives. There is no FFI, no eval-python, no escape hatch from inside SX.

  5. Fail fast. Violations are runtime errors (startup crash), not warnings. For typed targets, they're compile errors.

Adding a New Primitive

  1. Add declaration to primitives.sx (pure) or boundary.sx (I/O / page helper)
  2. Implement in the target language's primitive file
  3. The bootstrapper-emitted validator will accept it on next rebuild/restart
  4. If you skip step 1, the app crashes on startup telling you exactly what's missing

File Map

shared/sx/ref/
  primitives.sx    — Pure primitive declarations
  boundary.sx      — I/O primitive + page helper + boundary type declarations
  bootstrap_js.py  — JS bootstrapper (reads both, emits validation)
  bootstrap_py.py  — Python bootstrapper (reads both, emits validation)
  eval.sx          — Evaluator spec (symbol resolution, env model)
  parser.sx        — Parser spec
  render.sx        — Renderer spec (shared registries)