All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m33s
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>
86 lines
4.0 KiB
Markdown
86 lines
4.0 KiB
Markdown
# 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)
|
|
```
|