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

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)
```