Enforce SX boundary contract via boundary.sx spec + runtime validation
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>
This commit is contained in:
2026-03-05 23:50:02 +00:00
parent 54adc9c216
commit 04366990ec
21 changed files with 1342 additions and 415 deletions

View File

@@ -33,6 +33,7 @@ STYLE_ATOMS: dict[str, str] = {
"flex": "display:flex",
"inline-flex": "display:inline-flex",
"table": "display:table",
"table-row": "display:table-row",
"grid": "display:grid",
"contents": "display:contents",
"hidden": "display:none",
@@ -84,6 +85,7 @@ STYLE_ATOMS: dict[str, str] = {
"flex-row": "flex-direction:row",
"flex-col": "flex-direction:column",
"flex-wrap": "flex-wrap:wrap",
"flex-0": "flex:0",
"flex-1": "flex:1 1 0%",
"flex-shrink-0": "flex-shrink:0",
"shrink-0": "flex-shrink:0",
@@ -149,6 +151,7 @@ STYLE_ATOMS: dict[str, str] = {
"mt-2": "margin-top:.5rem",
"mt-3": "margin-top:.75rem",
"mt-4": "margin-top:1rem",
"mt-5": "margin-top:1.25rem",
"mt-6": "margin-top:1.5rem",
"mt-8": "margin-top:2rem",
"mt-[8px]": "margin-top:8px",
@@ -196,6 +199,7 @@ STYLE_ATOMS: dict[str, str] = {
"pb-8": "padding-bottom:2rem",
"pb-[48px]": "padding-bottom:48px",
"pl-2": "padding-left:.5rem",
"pl-3": "padding-left:.75rem",
"pl-5": "padding-left:1.25rem",
"pl-6": "padding-left:1.5rem",
"pr-1": "padding-right:.25rem",
@@ -216,11 +220,15 @@ STYLE_ATOMS: dict[str, str] = {
"w-10": "width:2.5rem",
"w-11": "width:2.75rem",
"w-12": "width:3rem",
"w-14": "width:3.5rem",
"w-16": "width:4rem",
"w-20": "width:5rem",
"w-24": "width:6rem",
"w-28": "width:7rem",
"w-32": "width:8rem",
"w-40": "width:10rem",
"w-48": "width:12rem",
"w-56": "width:14rem",
"w-1/2": "width:50%",
"w-1/3": "width:33.333333%",
"w-1/4": "width:25%",
@@ -241,6 +249,7 @@ STYLE_ATOMS: dict[str, str] = {
"h-10": "height:2.5rem",
"h-12": "height:3rem",
"h-14": "height:3.5rem",
"h-14": "height:3.5rem",
"h-16": "height:4rem",
"h-24": "height:6rem",
"h-28": "height:7rem",
@@ -268,11 +277,15 @@ STYLE_ATOMS: dict[str, str] = {
"max-w-3xl": "max-width:48rem",
"max-w-4xl": "max-width:56rem",
"max-w-full": "max-width:100%",
"max-w-0": "max-width:0",
"max-w-none": "max-width:none",
"max-w-screen-2xl": "max-width:1536px",
"max-w-[360px]": "max-width:360px",
"max-w-[768px]": "max-width:768px",
"max-w-[640px]": "max-width:640px",
"max-h-32": "max-height:8rem",
"max-h-64": "max-height:16rem",
"max-h-72": "max-height:18rem",
"max-h-96": "max-height:24rem",
"max-h-none": "max-height:none",
"max-h-[448px]": "max-height:448px",
@@ -282,6 +295,7 @@ STYLE_ATOMS: dict[str, str] = {
"text-xs": "font-size:.75rem;line-height:1rem",
"text-sm": "font-size:.875rem;line-height:1.25rem",
"text-base": "font-size:1rem;line-height:1.5rem",
"text-md": "font-size:1rem;line-height:1.5rem", # alias for text-base
"text-lg": "font-size:1.125rem;line-height:1.75rem",
"text-xl": "font-size:1.25rem;line-height:1.75rem",
"text-2xl": "font-size:1.5rem;line-height:2rem",
@@ -345,6 +359,7 @@ STYLE_ATOMS: dict[str, str] = {
"text-rose-500": "color:rgb(244 63 94)",
"text-rose-600": "color:rgb(225 29 72)",
"text-rose-700": "color:rgb(190 18 60)",
"text-rose-800": "color:rgb(159 18 57)",
"text-rose-800/80": "color:rgba(159,18,57,.8)",
"text-rose-900": "color:rgb(136 19 55)",
"text-orange-600": "color:rgb(234 88 12)",
@@ -355,6 +370,10 @@ STYLE_ATOMS: dict[str, str] = {
"text-yellow-700": "color:rgb(161 98 7)",
"text-green-600": "color:rgb(22 163 74)",
"text-green-800": "color:rgb(22 101 52)",
"text-green-900": "color:rgb(20 83 45)",
"text-neutral-400": "color:rgb(163 163 163)",
"text-neutral-500": "color:rgb(115 115 115)",
"text-neutral-600": "color:rgb(82 82 82)",
"text-emerald-500": "color:rgb(16 185 129)",
"text-emerald-600": "color:rgb(5 150 105)",
"text-emerald-700": "color:rgb(4 120 87)",
@@ -371,6 +390,7 @@ STYLE_ATOMS: dict[str, str] = {
"text-violet-600": "color:rgb(124 58 237)",
"text-violet-700": "color:rgb(109 40 217)",
"text-violet-800": "color:rgb(91 33 182)",
"text-violet-900": "color:rgb(76 29 149)",
# ── Background Colors ────────────────────────────────────────────────
"bg-transparent": "background-color:transparent",
@@ -413,6 +433,9 @@ STYLE_ATOMS: dict[str, str] = {
"bg-yellow-300": "background-color:rgb(253 224 71)",
"bg-green-50": "background-color:rgb(240 253 244)",
"bg-green-100": "background-color:rgb(220 252 231)",
"bg-green-200": "background-color:rgb(187 247 208)",
"bg-neutral-50/70": "background-color:rgba(250,250,250,.7)",
"bg-black/70": "background-color:rgba(0,0,0,.7)",
"bg-emerald-50": "background-color:rgb(236 253 245)",
"bg-emerald-50/80": "background-color:rgba(236,253,245,.8)",
"bg-emerald-100": "background-color:rgb(209 250 229)",
@@ -435,6 +458,12 @@ STYLE_ATOMS: dict[str, str] = {
"bg-violet-400": "background-color:rgb(167 139 250)",
"bg-violet-500": "background-color:rgb(139 92 246)",
"bg-violet-600": "background-color:rgb(124 58 237)",
"bg-violet-700": "background-color:rgb(109 40 217)",
"bg-amber-200": "background-color:rgb(253 230 138)",
"bg-blue-700": "background-color:rgb(29 78 216)",
"bg-emerald-700": "background-color:rgb(4 120 87)",
"bg-purple-700": "background-color:rgb(126 34 206)",
"bg-stone-50/60": "background-color:rgba(250,250,249,.6)",
# ── Border ───────────────────────────────────────────────────────────
"border": "border-width:1px",
@@ -445,6 +474,7 @@ STYLE_ATOMS: dict[str, str] = {
"border-b": "border-bottom-width:1px",
"border-b-2": "border-bottom-width:2px",
"border-r": "border-right-width:1px",
"border-l": "border-left-width:1px",
"border-l-4": "border-left-width:4px",
"border-dashed": "border-style:dashed",
"border-none": "border-style:none",
@@ -472,6 +502,9 @@ STYLE_ATOMS: dict[str, str] = {
"border-violet-200": "border-color:rgb(221 214 254)",
"border-violet-300": "border-color:rgb(196 181 253)",
"border-violet-400": "border-color:rgb(167 139 250)",
"border-neutral-200": "border-color:rgb(229 229 229)",
"border-red-400": "border-color:rgb(248 113 113)",
"border-stone-400": "border-color:rgb(168 162 158)",
"border-t-white": "border-top-color:rgb(255 255 255)",
"border-t-stone-600": "border-top-color:rgb(87 83 78)",
"border-l-stone-400": "border-left-color:rgb(168 162 158)",
@@ -499,17 +532,26 @@ STYLE_ATOMS: dict[str, str] = {
"opacity-0": "opacity:0",
"opacity-40": "opacity:.4",
"opacity-50": "opacity:.5",
"opacity-90": "opacity:.9",
"opacity-100": "opacity:1",
# ── Ring / Outline ───────────────────────────────────────────────────
"outline-none": "outline:2px solid transparent;outline-offset:2px",
"ring-2": "box-shadow:0 0 0 2px var(--tw-ring-color,rgb(59 130 246))",
"ring-offset-2": "box-shadow:0 0 0 2px rgb(255 255 255),0 0 0 4px var(--tw-ring-color,rgb(59 130 246))",
"ring-stone-300": "--tw-ring-color:rgb(214 211 209)",
"ring-stone-500": "--tw-ring-color:rgb(120 113 108)",
"ring-violet-500": "--tw-ring-color:rgb(139 92 246)",
"ring-blue-500": "--tw-ring-color:rgb(59 130 246)",
"ring-green-500": "--tw-ring-color:rgb(22 163 74)",
"ring-purple-500": "--tw-ring-color:rgb(147 51 234)",
# ── Overflow ─────────────────────────────────────────────────────────
"overflow-hidden": "overflow:hidden",
"overflow-x-auto": "overflow-x:auto",
"overflow-y-auto": "overflow-y:auto",
"overflow-visible": "overflow:visible",
"overflow-y-visible": "overflow-y:visible",
"overscroll-contain": "overscroll-behavior:contain",
# ── Text Decoration ──────────────────────────────────────────────────
@@ -655,8 +697,13 @@ PSEUDO_VARIANTS: dict[str, str] = {
"placeholder": "::placeholder",
"file": "::file-selector-button",
"aria-selected": "[aria-selected=true]",
"invalid": ":invalid",
"placeholder-shown": ":placeholder-shown",
"group-hover": ":is(.group:hover) &",
"group-open": ":is(.group[open]) &",
"group-open/cat": ":is(.group\\/cat[open]) &",
"group-open/filter": ":is(.group\\/filter[open]) &",
"group-open/root": ":is(.group\\/root[open]) &",
}