Tests cover page data cache, optimistic cache update/revert/confirm,
offline connectivity tracking, offline queue mutation, and offline-aware
routing. Registered in test runner with mocked platform functions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
defpage is already portable: server executes via execute_page(),
client via try-client-route. Add render plan logging to client
routing so console shows boundary decisions on navigation:
"sx:route plan pagename — N server, M client"
Mark Phase 7 (Full Isomorphism) as complete:
- 7a: affinity annotations + render-target
- 7b: page render plans (boundary optimizer)
- 7e: cross-host isomorphic testing (61 tests)
- 7f: universal page descriptor + visibility
7c (optimistic updates) and 7d (offline data) remain as future work.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
61 tests evaluate the same SX expressions on both Python (sx_ref.py)
and JS (sx-browser.js via Node.js), comparing output:
- 37 eval tests (arithmetic, strings, collections, logic, HO forms)
- 24 render tests (elements, attrs, void elements, components, escaping)
All pass — both hosts produce identical results from the same spec.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add page-render-plan to deps.sx: given page source + env + IO names,
computes a dict mapping each needed component to "server" or "client",
with server/client lists and IO dep collection. 5 new spec tests.
Integration:
- PageDef.render_plan field caches the plan at registration
- compute_page_render_plans() called from auto_mount_pages()
- Client page registry includes :render-plan per page
- Affinity demo page shows per-page render plans
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The entire parallel CSS system (StyleValue type, style dictionary,
keyword atom resolver, content-addressed class generation, runtime
CSS injection, localStorage caching) was built but never adopted —
the codebase already uses :class strings with defcomp components
for all styling. Remove ~3,000 lines of unused infrastructure.
Deleted:
- cssx.sx spec module (317 lines)
- style_dict.py (782 lines) and style_resolver.py (254 lines)
- StyleValue type, defkeyframes special form, build-keyframes platform fn
- Style dict JSON delivery (<script type="text/sx-styles">), cookies, localStorage
- css/merge-styles primitives, inject-style-value, fnv1a-hash platform interface
Simplified:
- defstyle now binds any value (string, function) — no StyleValue type needed
- render-attrs no longer special-cases :style StyleValue → class conversion
- Boot sequence skips style dict init step
Preserved:
- tw.css parsing + CSS class delivery (SX-Css headers, <style id="sx-css">)
- All component infrastructure (defcomp, caching, bundling, deps)
- defstyle as a binding form for reusable class strings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add :affinity :client/:server/:auto annotations to defcomp, with
render-target function combining affinity + IO analysis. Includes
spec (eval.sx, deps.sx), tests, Python evaluator, and demo page.
Fix critical bug: Python SX parser _ESCAPE_MAP was missing \r and \0,
causing bootstrapped JS parser to treat 'r' as whitespace — breaking
all client-side SX parsing. Also add \0 to JS string emitter and
fix serializer round-tripping for \r and \0.
Reserved word escaping: bootstrappers now auto-append _ to identifiers
colliding with JS/Python reserved words (e.g. default → default_,
final → final_), so the spec never needs to avoid host language keywords.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
_script_hash used relative Path("static") which resolved to /app/static/
inside the container, but the file is at /app/shared/static/. Use
Path(__file__).parent.parent to resolve from shared/ correctly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Parser: chained .replace() calls processed \n before \\, causing \\n
to become a real newline. Replaced with character-by-character
_unescape_string. Fixes 2 parser spec test failures.
Primitives: prim_get only handled dict and list. Objects with .get()
methods (like PageDef) returned None. Added hasattr fallback.
Fixes 9 defpage spec test failures.
All 259 spec tests now pass (was 244/259).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
domCreateElement treated SX NIL (a truthy JS object) as a real namespace,
calling createElementNS("nil", tag) instead of createElement(tag). All
elements created by resolveSuspense ended up in the "nil" XML namespace
where CSS class selectors don't match.
Also fix ~suspense fallback: empty &rest list is truthy in SX, so
fallback content never rendered.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
parse() returns a list of expressions. resolveSuspense was passing the
entire array to renderToDom, which interpreted [(<> ...)] as a call
expression ((<> ...)) — causing "Not callable: {}". Now iterates
each expression individually, matching the public render() API.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The make-raw-html wrapper in eval-list was host-specific: it fixed
server-side HTML escaping but broke the client DOM adapter (render-expr
returns DOM nodes, not strings). The raw-html wrapping belongs in the
host (async_eval_ref.py line 94-101), not the spec.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Streaming resolve scripts arrive after boot, so any extra component
defs sent as <script type="text/sx"> tags weren't being loaded.
Fix in the spec (boot.sx): call (process-sx-scripts nil) at the
start of resolve-suspense so late-arriving component defs are
available in componentEnv before rendering.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolved SX content may reference components not in the initial shell
scan (e.g. ~cart-mini from IO-generated headers). Diff the needed
components against what the shell already sent and prepend any extra
defcomps as a <script type="text/sx"> block before the resolve script.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The streaming page's component scan only covered the suspense
placeholders, missing transitive deps from layout headers (e.g.
~cart-mini, ~auth-menu). Add layout.component_names to Layout class
and include them plus page content_expr in the scan source.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The spec's eval-list calls render-expr for HTML tags/components in eval
position but returned a plain string. When that string was later passed
through _arender (e.g. as a component keyword arg), it got HTML-escaped.
Fix in eval.sx: wrap render-expr result in make-raw-html so the value
carries the raw-html type through any evaluator boundary. Also add
is_render_expr check in async_eval_ref.py as belt-and-suspenders for
the same issue in the async wrapper.
This fixes the streaming demo where suspense placeholder divs were
displayed as escaped text instead of real DOM elements.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The streaming shell now uses render_to_html so [data-suspense] elements
are real DOM elements immediately when the browser parses the HTML.
Previously the shell used SX wire format in a <script data-mount> tag,
requiring sx-browser.js to boot and render before suspense elements
existed — creating a race condition where resolution scripts fired
before the elements were in the DOM.
Now: server renders HTML with suspense placeholders → browser has real
DOM elements → resolution scripts find and replace them reliably.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Async generator bodies don't execute until __anext__(), by which time
the request context is gone. Restructure execute_page_streaming as a
regular async function that does all context-dependent work (g, request,
current_app access, layout resolution, task creation) while the context
is live, then returns an inner async generator that only yields strings
and awaits pre-created tasks.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
stream_with_context decorates a generator function, not a generator
instance. Wrap execute_page_streaming in a decorated inner function.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Quart's ASGI layer doesn't propagate request/app context into async
generator iterations. Wrap execute_page_streaming with stream_with_context
so current_app and request remain accessible after each yield.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
boundary.sx now contains only generic web-platform I/O primitives that
any SX host would provide (current-user, request-arg, url-for, etc.).
Moved to boundary-app.sx (deployment-specific):
- Inter-service: frag, query, action, service
- Framework: htmx-request?, g, jinja-global
- Domain: nav-tree, get-children, relations-from
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Client-side routing was only swapping #main-panel content without
updating OOB headers (nav rows, sub-rows). Now each page entry in the
registry includes a layout identity (e.g. "sx-section:Testing") and
try-client-route falls through to server when layout changes, so OOB
header updates are applied correctly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Server streams HTML shell with ~suspense placeholders immediately,
then sends resolution <script> chunks as async IO completes. Browser
renders loading skeletons instantly, replacing them with real content
as data arrives via __sxResolve().
- defpage :stream true opts pages into streaming response
- ~suspense component renders fallback with data-suspense attr
- resolve-suspense in boot.sx (spec) + bootstrapped to sx-browser.js
- __sxPending queue handles resolution before sx-browser.js loads
- execute_page_streaming() async generator with concurrent IO tasks
- Streaming demo page at /isomorphism/streaming with 1.5s simulated delay
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New /testing/ section with 6 pages: overview (all specs), evaluator,
parser, router, renderer, and runners. Each page runs tests server-side
(Python) and offers a browser "Run tests" button (JS). Modular browser
runner (sxRunModularTests) loads framework + per-spec sources from DOM.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The IO handler and bridge both imported asset_url from
shared.infrastructure.urls, but it doesn't exist there — it's a Jinja
global defined in jinja_setup.py. Use current_app.jinja_env.globals.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sx-browser.js evaluates test.sx directly in the browser — click
"Run 81 tests" to see SX test itself. Uses the same Sx global that
rendered the page.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
test.sx now defines deftest/defsuite as macros. Any host that provides
5 platform functions (try-call, report-pass, report-fail, push-suite,
pop-suite) can evaluate the file directly — no bootstrap compilation
step needed for JS.
- Added defmacro for deftest (wraps body in thunk, catches via try-call)
- Added defmacro for defsuite (push/pop suite context stack)
- Created run.js: sx-browser.js evaluates test.sx directly (81/81 pass)
- Created run.py: Python evaluator evaluates test.sx directly (81/81 pass)
- Deleted bootstrap_test_js.py and generated test_sx_spec.js
- Updated testing docs page to reflect self-executing architecture
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The test framework is written in SX and tests SX — the language proves
its own correctness. test.sx defines assertion helpers (assert-equal,
assert-true, assert-type, etc.) and 15 test suites covering literals,
arithmetic, comparison, strings, lists, dicts, predicates, special forms,
lambdas, higher-order forms, components, macros, threading, truthiness,
and edge cases.
Two bootstrap compilers emit native tests from the same spec:
- bootstrap_test.py → pytest (81/81 pass)
- bootstrap_test_js.py → Node.js TAP using sx-browser.js (81/81 pass)
Also adds missing primitives to spec and Python evaluator: boolean?,
string-length, substring, string-contains?, upcase, downcase, reverse,
flatten, has-key?. Fixes number? to exclude booleans, append to
concatenate lists.
Includes testing docs page in SX app at /specs/testing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sx-browser.js grew past OS arg length limit for node -e. Write to
temp file instead. Also fix Sx global scope: Node file mode sets
`this` to module.exports, not globalThis, so the IIFE wrapper needs
.call(globalThis) to make Sx accessible to sx-test.js.
855 passed (2 pre-existing empty .sx file failures).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>