Commit Graph

84 Commits

Author SHA1 Message Date
6e27442d57 Step 17: streaming render — hyperscript enhancements, WASM builds, live server tests
Streaming chunked transfer with shell-first suspense and resolve scripts.
Hyperscript parser/compiler/runtime expanded for conformance. WASM static
assets added to OCaml host. Playwright streaming and page-level test suites.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 08:41:38 +00:00
7aefe4da8f Fix streaming: resolve scripts inside </body>, live server tests
The shell HTML included closing </body></html> tags. Resolve script
chunks arrived AFTER the document end — browser ignored them
(ERR_INCOMPLETE_CHUNKED_ENCODING). Now strips </body></html> from
shell, sends resolve scripts inside the body, closes document last.

Added live server Playwright tests that hit the actual streaming
endpoint and verify suspense slots resolve with content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 16:40:49 +00:00
d4c0be52b1 Fix ListRef handling in streaming data — list from SX is ListRef in OCaml
The streaming render matched `List items` but SX's `(list ...)` produces
`ListRef` (mutable list) in the OCaml runtime. Data items were rejected
with "returned list, expected dict or list" — 0 resolve chunks sent.

Fixed both streaming render and AJAX paths to handle ListRef.
Added sandbox test for streaming-demo-data return type validation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 16:34:50 +00:00
3cada3f8fe Async IO in streaming render — staggered resolve with io-sleep
Server (sx_server.ml):
- eval_with_io: CEK evaluator with IO suspension handling (io-sleep, import)
- io-sleep platform primitive: raises CekPerformRequest, resolved by eval_with_io
- Streaming render uses eval_with_io for data + content evaluation
- Data items with "delay" field sleep before resolving (async streaming)
- Removed hardcoded streaming-demo-data — application logic belongs in .sx

Application (streaming-demo.sx):
- streaming-demo-data defined in SX: 3 items with 1s/3s/5s delays
- Each item has delay, stream-id, and display data fields
- Shell renders instantly, slots fill progressively as IO completes

Tests (streaming.spec.js):
- Staggered resolve test: fast resolves first, medium/slow still skeleton
- Verifies independent slot resolution matches async IO behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 16:26:44 +00:00
eaf5af4cd8 Step 17: streaming render — chunked transfer, shell-first suspense, resolve scripts
Server (sx_server.ml):
- Chunked HTTP transport (Transfer-Encoding: chunked)
- Streaming page detection via scan_defpages (:stream true)
- Shell-first render: outer layout + shell AST → aser → SSR → flush
- Data resolution: evaluate :data, render :content per slot, flush __sxResolve scripts
- AJAX streaming: synchronous eval + OOB swaps for SPA navigation
- SX URL → flat path conversion for defpage matching
- Error boundaries per resolve section
- streaming-demo-data helper for the demo page

Client (sx-platform.js):
- Sx.resolveSuspense: finds [data-suspense] element, parses SX, renders to DOM
- Fallback define for resolve-suspense when boot.sx imports fail in WASM
- __sxPending drain on boot (queued resolves from before sx.js loads)
- __sxResolve direct dispatch after boot

Tests (streaming.spec.js):
- 5 sandbox tests using real WASM kernel
- Suspense placeholder rendering, __sxResolve replacement, independent slot resolution
- Full layout with gutters, end-to-end resolve with streaming-demo/chunk components

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 16:12:28 +00:00
36ae0384ae Smarter implicit then: only before command keywords — 341/831 (41%)
Fixed then insertion to only trigger before known HS command keywords
(set, put, add, remove, toggle, etc.) via lookahead regex, instead of
on all multi-space sequences. Prevents breaking single-command
expressions with wide spacing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 23:26:06 +00:00
299f3e748d Implicit then, between, starts/ends with — 339/831 (41%)
Biggest win: HS sources from upstream HTML had newlines replaced with
spaces, losing command separation. Now multi-space sequences become
'then' keywords, matching _hyperscript's implicit newline-as-separator
behavior. +42 tests passing.

Parser: 'is between X and Y', 'is not between', 'starts with',
'ends with' comparison operators.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 23:15:01 +00:00
52e4d38852 Eval-only tests, multi-class, async IO — 295/831 (35%)
- Generator: converts no-HTML tests with run("expr").toBe(val) patterns
  to (assert= val (eval-hs "expr")). 111→92 stubs (-19 converted).
- Parser: multi-class add/remove (.foo .bar collects into multi-add-class)
- Compiler: multi-add-class/multi-remove-class emit (do (dom-add-class..))
- Test runner: drives IO suspension in per-test evaluate for async tests
- Parser: catch/finally support in on handlers, cmd terminators

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 22:36:56 +00:00
5fe97d8481 Multi-class add/remove, async IO in test runner — 280/831 (34%)
- Parser: add .foo .bar collects multiple class refs into multi-add-class AST
- Compiler: multi-add-class/multi-remove-class emit (do (dom-add-class...) ...)
- Test runner: drives IO suspension chains (wait/fetch/settle) via _driveAsync
  so async HS tests (wait 100ms, settle, fetch) can complete
- Assertion failed: 51→49

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 22:24:16 +00:00
08cd82ed65 Parser: catch/finally in on handlers, cmd terminators — 279/831 (34%)
- parse-cmd: catch/finally/end/else/otherwise are now terminators that
  stop parse-cmd-list (return nil from parse-cmd)
- parse-on-feat: optional catch var handler / finally handler clauses
  after the command body, before 'end'
- emit-on: scan-on passes catch-info/finally-info through recursion,
  wraps compiled body in (guard (var (true catch-body)) body) when
  catch clause is present
- Runtime: hs-put! handles "start" (afterbegin) and "end" (beforeend)
- Removed duplicate conformance-dev.sx (all 110 tests already in behavioral)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 21:51:43 +00:00
f97a1711c6 Error sampling for bar/assertion failures — 309/941
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:30:11 +00:00
1461919857 eval-hs helper + generator fixes — 304/941 (32%)
Added eval-hs: compile and evaluate HS expressions/commands, used by
conformance-dev tests. Smart wrapping: adds 'return' prefix for
expressions, leaves commands (set/put/get/then/return) as-is.

Fixed generator ref() to use context-aware variable mapping.

304/941 with the user's conformance-dev.sx tests included (110 new).
Failure breakdown: 111 stubs, 74 "bar" (eval errors), 51 assertion
failures, 30 eval-only stubs, 24 undefined "live", 18 parser errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:09:03 +00:00
ce4579badb Generator: context-aware variable refs — 444/831 (53%, +20)
Fixed ref() to map upstream JS variable names to let-bound SX variables
using element context (tag→var, id→var, make-return→last-var). Fixes
if (0→14/19), put (14→18), on (20→23), and other categories where the
upstream test uses make() return variables like d1, div, btn.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:11:03 +00:00
e98aedf803 Add comprehensive Playwright hydration tests — 15 tests
Covers all bugs fixed in the DOM-preserving hydration work:

DOM preservation:
- Islands hydrate without errors or warnings
- Both islands report hydrated in boot log
- No replaceChildren called on island elements
- No stray comment markers in island DOM

Counter text nodes (was: "0 / 16" → "0"):
- Counter shows full "0 / 16" text
- Counter has exactly 3 text nodes (value, separator, total)
- Counter updates on forward/back clicks

Event listeners (was: buttons had no click handlers):
- Stepper buttons respond to clicks
- Header navigation links present after hydration

Code view:
- Syntax-highlighted spans present after hydration
- Code highlighting advances with stepper clicks

SSR DOM identity:
- Element count roughly preserved (not doubled)
- Stepper buttons are the SAME DOM nodes (JS property survives)
- Header elements are the SAME DOM nodes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 14:29:53 +00:00
9c64d1d929 Fix stepper preview flash: render lake on client, screenshot-based test
Root cause: the lake had (when (not (client?)) ...) guard — SSR rendered
"the joy of sx" preview but client skipped it. replaceChildren swapped
in an empty lake. The rebuild-preview effect was skipped (first-run
optimization), so the preview stayed blank for ~500ms.

Fix: remove the client? guard so the lake renders on both server and
client. The template's steps-to-preview produces the initial preview.
The effect only fires on subsequent step changes (not first run).

Test: replaced MutationObserver approach with screenshot comparison.
Loads page with JS blocked (pure SSR), takes screenshot. Loads with JS
(hydration), takes screenshot. Compares pixels. Any visual difference
fails the test.

Result: "No visual flash: screenshots identical" — passes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 09:25:43 +00:00
42198e4e22 Fix hydration flash: skip initial effect run when state matches SSR
Root cause: the stepper's rebuild effect (update-code-highlight,
rebuild-preview) fired immediately on hydration via schedule-idle,
modifying the DOM after replaceChildren swapped in identical content.
This caused a visible text change after the initial frame.

Fix: track initial step-idx value and first-run flag. Skip the
effect on first run if the current step matches the SSR state
(from cookie). The effect only fires on actual user interaction.

Result: SSR and hydrated text content are identical. replaceChildren
swaps DOM nodes but the visual content doesn't change. Zero flash.

Test: "No clobber: clean" — 0 text changes during hydration.
All 8 home features pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 09:08:34 +00:00
e6def8b6cd Test infra: deferred execution, per-test timeout, error classification
424/831 (51%): 290 crash, 111 stub, 6 timeout.
Deferred architecture: tests register thunks during load, run individually
with 3s Promise.race timeout. Page reboots after hangs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:53:54 +00:00
2805e0077b Fix clobber test: detect text content change, not just empty state
The previous test only checked if childNodes.length hit zero. With
replaceChildren that never happens — but the flash is still visible
because the SSR DOM is replaced with different reactive DOM.

New test captures SSR textContent before JS boots, watches for any
change via MutationObserver. Now correctly fails:
  "text changed — ssr:(div (~tw :tokens... → hydrated:..."

This proves the flash: island hydration replaces SSR DOM wholesale.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:08:02 +00:00
737964be89 Honest test suite: 424/831 (51%) — all tests run, timeouts fail visibly
Rewrote test architecture: deferred execution. Tests register thunks during
file load (try-call redefined to append to _test-registry), then the
Playwright loop runs each individually with 3s timeout via Promise.race.
Hanging tests (parser infinite loops) fail with TIMEOUT and trigger page
reboot. No tests are hidden or skipped.

Fixed generator: proper quote escaping for HS sources with embedded quotes,
sanitized comments to avoid SX parser special chars.

831 tests registered, 424 pass, 407 fail honestly:
- 22 perfect categories (empty, dialog, morph, default, reset, scroll, etc.)
- Major gaps: if 0/19, wait 0/7, take 0/12, repeat 2/30, set 4/25
- Timeout failures from parser hangs on unsupported syntax

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:54:01 +00:00
23c88cd1e5 Atomic island hydration: replaceChildren instead of clear+append
The hydrate-island function was doing:
  (dom-set-text-content el "")  ;; clears SSR content — visible flash
  (dom-append el body-dom)       ;; adds reactive DOM

Now uses:
  (host-call el "replaceChildren" body-dom)  ;; atomic swap, no empty state

Per DOM spec, replaceChildren is a single synchronous operation — the
browser never renders the intermediate empty state. The MutationObserver
test now checks for content going to zero (visible gap), not mutation
count (mutations are expected during any swap).

Test: "No clobber: clean" — island never goes empty during hydration.
All 8 home features pass: no-flash, no-clobber, boot, islands, stepper,
smoke, no-errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:51:37 +00:00
3329512bf8 Add hydration clobber detection test — 55 DOM removals detected
MutationObserver injected before page JS boots watches the stepper
island for content removal during hydration. Detects 55 node removals
— the island hydration destroys SSR DOM and rebuilds it, causing a
visible flash.

Test correctly fails: "No clobber: 55 removals"
This is the root cause of the flash — island hydration needs to
preserve SSR content instead of replacing it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:40:52 +00:00
79ba9c2d40 Fix stepper SSR/hydration flash: server reads cookie, cache bypass
Three changes to eliminate the stepper flash:

1. home-stepper.sx: server path reads cookie via (get-cookie) for
   step-idx initial value. Client path reads document.cookie via
   def-store. Both default to 0 when no cookie exists.

2. sx_server.ml: bypass response cache when sx-home-stepper cookie
   is present. Render on main thread (not worker) so get-cookie
   sees the parsed request cookies.

3. site-full.spec.js: flash detection test sets cookie=7 via
   Playwright context, checks SSR HTML matches hydrated state.

Test: "No flash: SSR=7 hydrated=7 (cookie=7)" — passes.
Tested on fresh stack=site server subprocess.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:28:47 +00:00
32fd3ef7d3 Add SSR/hydration flash detection test, fix to-number → parse-number
- site-full.spec.js: home test captures SSR counter from raw HTML before
  JS runs, compares with post-hydration counter. Fails if they differ.
- home-stepper.sx: to-number → parse-number (to-number doesn't exist
  in the OCaml server environment — caused crash on fresh server start)

Test output: "No flash: SSR=0 hydrated=0" — passes.
Tested on fresh stack=site server, not cached Docker container.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:22:25 +00:00
0a2d7768dd Rewrite test suite: data-driven discovery, all 8 HS elements, SPA fixed
Tests are now fully automated — discover features from the DOM:
- discoverPage(): finds islands, HS elements, sx-get links, content
- testHsElement(): clicks each _="..." element, checks for any DOM change
- testHsWaitElement(): handles async wait cycles (add/wait/remove)
- SPA: uses Playwright locator.click() on a[sx-get] links — 5/5 pass

Results: 5 pass, 3 fail (all real bugs):
  home: stepper click detection needs ▶ selector fix
  hyperscript HS[6]: put "<b>Rendered!</b>" into #target — no effect
  language: spec.explore.evaluator page hangs (server bug)
  SPA navigation: 5/5 sections pass
  geography 11/11, applications 8/8, tools 4/4, etc 4/4

7/8 HS elements pass. HS[6] (put into target) is a real compiler bug.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 06:41:57 +00:00
fecfc71e5f Add full site test suite: stack=site sandbox, per-page feature reports
New test infrastructure:
- site-server.js: shared OCaml HTTP server lifecycle (beforeAll/afterAll)
- site-full.spec.js: full site test suite, no Docker

Tests:
  home (7 features): boot, header island, stepper island, stepper click,
    SPA navigation, universal smoke, no console errors
  hyperscript (8 features): boot, HS element discovery, activation (8/8),
    toggle color on/off, count clicks, bounce add/wait/remove, smoke, errors
  geography: 12/12 pages render
  applications: 9/9 pages render
  tools: 5/5 pages render
  etc: 5/5 pages render
  SPA navigation: SKIPPED (link boosting not working yet)
  language: FAILS — /sx/(language.(spec.(explore.evaluator))) hangs (real bug)

Run: npx playwright test tests/playwright/site-full.spec.js
Run one: npx playwright test tests/playwright/site-full.spec.js -g "hyperscript"

Each test prints a feature report showing exactly what was verified.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 22:20:13 +00:00
7492ceac4e Restore hyperscript work on stable site base (908f4f80)
Reset to last known-good state (908f4f80) where links, stepper, and
islands all work, then recovered all hyperscript implementation,
conformance tests, behavioral tests, Playwright specs, site sandbox,
IO-aware server loading, and upstream test suite from f271c88a.

Excludes runtime changes (VM resolve hook, VmSuspended browser handler,
sx_ref.ml guard recovery) that need careful re-integration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 19:29:56 +00:00
52165f6a2a Restore _driveAsync in sandbox host-callback
With the pending_cek snapshot fix, _driveAsync no longer causes
duplicate resume chains. Needed for event-triggered suspensions
(btn.click → handler → perform) where the suspension propagates
through addEventListener, invisible to the outer eval.

Sandbox bytecode test: 6/6 io-sleep suspensions confirmed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 21:40:17 +00:00
7a1af7a80a WIP: bytecode when/do/perform — host-callback _driveAsync fix + debugging
Root cause identified: nested cek_call_or_suspend calls on same VM
overwrite pending_cek. First call suspends (thunk's hs-wait), second
call from synchronous dom-listen callback overwrites before resume.

sandbox host-callback: removed _driveAsync call to prevent duplicate
resume chains. Still 3/6 in Node.js test — issue is in OCaml call
stack nesting, not JS async.

Next: prevent pending_cek overwrite in nested CEK→VM→CEK→VM chains.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 21:21:08 +00:00
520424954b Sandbox bytecode loading: K.load + load-sxbc, bytecode param, web stack sxbc via loadModule
Bytecode modules now load correctly in sandbox mode. HS .sxbc modules
use K.load('(load-sxbc ...)') which syncs defines to eval env. Web stack
.sxbc modules use K.loadModule with import suspension drive loop.

K.eval used directly for expression eval (not thunk wrapper) so bytecode-
defined symbols are visible. Falls back to callFn thunk on IO suspension.

Sandbox now reproduces the bytecode repeat bug: source gives 6/6
suspensions, bytecode gives 4/6. Bug is in bytecode compilation of
when/do across perform boundaries, not the runtime wrapper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 20:56:54 +00:00
c521ff8731 Fix hs-repeat-times: wrap when multi-body in (do ...) for IO suspension
The when form's continuation for the second body expression was lost
across perform/cek_resume cycles. Wrapping (thunk) and (do-repeat)
in an explicit (do ...) gives when a single body, and do's own
continuation handles the sequencing correctly.

Sandbox confirms: 6/6 io-sleep suspensions now chain through
host-callback → _driveAsync → resume_vm (was 1/6 before fix).

Also fix sandbox async timing: _asyncPending counter tracks in-flight
IO chains so page.evaluate waits for all resumes to complete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 20:31:12 +00:00
4baed1853c OCaml runtime: R7RS parameters, VM closure introspection, import suspension
- R7RS parameter primitives (make-parameter, parameter?, parameterize support)
- VM closure get_val introspection (vm-code, vm-upvalues, vm-name, vm-globals)
- Lazy list caching on vm_code for transpiled VM performance
- VM import suspension: check_io_suspension + resume_module for browser lazy loading
- 23 new R7RS tests (parameter-basic, parameterize-basic, syntax-rules-basic)
- Playwright bytecode-loading spec + WASM rebuild

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 18:48:51 +00:00
2727577702 VM import suspension for browser lazy loading
Bytecode compiler now emits OP_PERFORM for (import ...) and compiles
(define-library ...) bodies. The VM stores the import request in
globals["__io_request"] and stops the run loop — no exceptions needed.
vm-execute-module returns a suspension dict, vm-resume-module continues.

Browser: sx_browser.ml detects suspension dicts from execute_module and
returns JS {suspended, op, request, resume} objects. The sx-platform.js
while loop handles cascading suspensions via handleImportSuspension.

13 modules load via .sxbc bytecode in 226ms (manifest-driven), both
islands hydrate, all handlers wired. 2650/2650 tests pass including
6 new vm-import-suspension tests.

Also: consolidated sx-platform-2.js → sx-platform.js, fixed
vm-execute-module missing code-from-value call, fixed bootstrap.py
protocol registry transpiler issues.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 17:11:12 +00:00
17b6c872f2 Server cleanup: extract app-specific config into SX
Phase 1 Step 2 of architecture roadmap. The OCaml HTTP server is now
generic — all sx_docs-specific values (layout components, path prefix,
title, warmup paths, handler prefixes, CSS/JS, client libs) move into
sx/sx/app-config.sx as a __app-config dict. Server reads config at
startup with hardcoded defaults as fallback, so it works with no config,
partial config, or full config.

Removed: 9 demo data stubs, stepper cookie cache logic, page-functions.sx
directory heuristic. Added: 29-test server config test suite covering
standard, custom, no-config, and minimal-config scenarios.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 21:00:32 +00:00
6d5c410d68 Uncommitted sx-tools changes: WASM bundles, Playwright specs, engine fixes
WASM browser bundles rebuilt with latest kernel. Playwright test specs
updated (helpers, navigation, handler-responses, hypermedia-handlers,
isomorphic, SPA navigation). Engine/boot/orchestration SX files updated.
Handler examples and not-found page refreshed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 18:58:38 +00:00
14d5158b06 Fix 5 Playwright marshes test failures: timing + test logic
on-settle: increase wait from 2s to 4s — server fetch + settle hook
needs more time than the original timeout allowed.

server-signals: add actual cross-island signal test — click a price
button in writer island and verify reader island updates.

view-transform: fetch catalog before toggling view — the view toggle
only changes rendering of loaded items, not the empty state.

All 19 demo-interaction tests pass (was 14/19).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 18:54:05 +00:00
b6e144a6fd SPA nav improvements: scroll restoration, popstate, history spec
- boot.sx: popstate handler extracts scrollY from history state
- engine.sx: pass scroll position to handle-popstate
- boot-helpers.sx: scroll position tracking in navigation
- orchestration.sx: scroll state management for back/forward nav
- history.spec.js: new Playwright spec for history navigation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:36:24 +00:00
547d271571 Fix stepper: lake SSR preservation + stack rebuild after stepping
Three fixes:

1. Framework: render-dom-lake preserves SSR elements during hydration.
   When client-side render-to-dom encounters a lake with an existing
   DOM element (from SSR), it reuses that element instead of creating
   a new one. This prevents the SSR HTML from being replaced with
   unresolvable raw SX expressions (~tw calls).

2. Stepper: skip rebuild-preview on initial hydration. Uses a non-
   reactive dict flag (not a signal) to avoid triggering the effect
   twice. On first run, just initializes the DOM stack from the
   existing SSR content by computing open-element depth from step
   types and walking lastElementChild.

3. Stepper: rebuild-preview computes correct DOM stack after re-render.
   Same depth computation + DOM walk approach. This fixes the bug where
   do-step after do-back would append elements to the wrong parent
   (e.g. "sx" span outside h1).

Also: increased code view font-size from 0.5rem to 0.85rem.

Playwright tests:
- lake never shows raw SX during hydration (mutation observer)
- back 6 + forward 6 keeps all 4 spans inside h1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:18:16 +00:00
b13962e8dd Fix stepper hydration flash and stepping structure
Two bugs fixed:

1. Hydration flash: The effect's schedule-idle called rebuild-preview
   on initial hydration, which cleared the SSR HTML and re-rendered
   (flashing raw SX source or blank). Fix: skip rebuild-preview on
   initial render — the SSR content is already correct. Only rebuild
   when stepping.

2. Stepping structure: After do-back calls rebuild-preview, the DOM
   stack was reset to just [container]. Subsequent do-step calls
   appended elements to the wrong parent (e.g. "sx" span outside h1).
   Fix: compute the correct stack depth by replaying open/close step
   counts, then walk the rendered DOM tree that many levels deep via
   lastElementChild to find the actual DOM nodes.

Proven by harness test: compute-depth returns 3 at step 10 (inside
div > h1 > span), 2 at step 8 (inside div > h1), 0 at step 16 (all
closed).

Playwright test: lake never shows raw SX (~tw, :tokens) during
hydration — now passes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:42:40 +00:00
9a64f13dc6 Fix stepper default: show full "the joy of sx" on load
Root cause: default step-idx was 9, but the expression has 16 steps.
At step 9, only "the joy" + empty emerald span renders. Changed default
to 16 so all four words display after hydration.

Reverted mutable-list changes — (list) already creates ListRef in the
OCaml kernel, so append! works correctly with plain (list).

Added spec/tests/test-stepper.sx (7 tests) proving the split-tag +
steps-to-preview pipeline works correctly at each step boundary.

Updated Playwright stepper.spec.js with four tests:
- no raw SX visible after hydration
- default view shows all four words
- all spans inside h1
- stepping forward renders styled text

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:55:40 +00:00
d40a9c6796 sx-tools: WASM kernel updates, TW/CSSX rework, content refresh, new debugging tools
Build tooling: updated OCaml bootstrapper, compile-modules, bundle.sh, sx-build-all.
WASM browser: rebuilt sx_browser.bc.js/wasm, sx-platform-2.js, .sxbc bytecode files.
CSSX/Tailwind: reworked cssx.sx templates and tw-layout, added tw-type support.
Content: refreshed essays, plans, geography, reactive islands, docs, demos, handlers.
New tools: bisect_sxbc.sh, test-spa.js, render-trace.sx, morph playwright spec.
Tests: added test-match.sx, test-examples.sx, updated test-tw.sx and web tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:31:57 +00:00
9594362427 Spec explorer: full source in drill-in, explore any .sx file
- spec-explore-define uses serialize (full body) instead of signature
- _spec-search-dirs expanded: spec/, web/, lib/, shared/sx/ref/, sx/, sxc/, shared/sx/templates/
- explore() works with any filename, not just nav spec items
- Playwright tests use flexible regex for line/define counts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 00:10:45 +00:00
6e885f49b6 Spec explorer: fix SxExpr rendering bugs, add drill-in UX, Playwright tests
Fix 3 OCaml bugs that caused spec explorer to hang:
- sx_types: inspect outputs quoted string for SxExpr (not bare symbol)
- sx_primitives: serialize/to_string extract SxExpr/RawHTML content
- sx_render: handle SxExpr in both render-to-html paths

Restructure spec explorer for performance:
- Lightweight overview: name + kind only (was full source for 141 defs)
- Drill-in detail: click definition → params, effects, signature
- explore() page function accepts optional second arg for drill-in
- spec() passes through non-string slugs from nested routing

Fix aser map result wrapping:
- aser-special map now wraps results in fragment (<> ...) via aser-fragment
- Prevents ((div ...) (div ...)) nested lists that caused client "Not callable"

5 Playwright tests: overview load, no errors, SPA nav, drill-in detail+params

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 23:46:13 +00:00
f66195ce18 Fix delete-row handler: nested slug extraction in OCaml dispatch
The slug extractor after (api. scanned for the first ) but for nested
URLs like (api.(delete.1)) it got "(delete.1" instead of "delete".
Now handles nested parens: extracts handler name and injects path
params into query string. Also strengthened the Playwright test to
accept confirm dialogs and assert strict row count decrease.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 19:41:32 +00:00
46f77c3b1e Adapter fixes, orchestration updates, example content + SPA tests
From other session: adapter-html/sx/dom fixes, orchestration
improvements, examples-content refactoring, SPA navigation test
updates, WASM copies synced.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:35:49 +00:00
584445a843 SPA navigation, page component refactors, WASM rebuild
Refactor page components (docs, examples, specs, reference, layouts)
and adapters (adapter-sx, boot-helpers, orchestration) across sx/ and
web/ directories. Add Playwright SPA navigation tests. Rebuild WASM
kernel with updated bytecode. Add OCaml primitives for request handling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:00:51 +00:00
8027f51ef3 Playwright tests for all 27 hypermedia handler examples
Comprehensive interaction tests covering GET handlers (click-to-load,
tabs, polling, search, filter, keyboard, lazy-load), POST handlers
(form-submission, inline-validation, reset), mutation handlers
(delete-row, edit-row, inline-edit, bulk-update, put-patch), and
special handlers (dialogs, oob-swaps, animations, progress-bar, retry,
json-encoding, vals-and-headers).

Tests use trackErrors + waitForSxReady. Will pass once server is
restarted with request primitives and handler dispatch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 00:59:36 +00:00
75827b4828 Fix source display: add sxc mount, HTML tag forms, Playwright debug tools
- Add missing ./sx/sxc:/app/sxc:ro volume mount to dev-sx compose —
  container was using stale image copy of docs.sx where ~docs/code had
  (&key code) instead of (&key src), so highlight output was silently
  discarded
- Register HTML tags as special forms in WASM browser kernel so keyword
  attrs are preserved during render-to-dom
- Add trace-boot, hydrate-debug, eval-at modes to sx-inspect.js for
  debugging boot phases and island hydration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 21:39:32 +00:00
3598a34e3d Add failing tests: set! in named let loop (WASM + native OCaml)
Named let creates a loop continuation but set! inside the loop body
does not mutate bindings in the enclosing let scope. Affects both
the WASM kernel and native OCaml CEK evaluator.

6 failing Node tests cover:
- set! counter (simplest case)
- set! counter with named let params
- set! list accumulator via append
- append! + set! counter combo
- set! string concatenation
- nested named let set!

3 baselines pass: plain let set!, functional named let, plain append!

Also adds spec/tests/test-named-let-set.sx (7 assertions, first
fails and aborts — confirms bug exists in spec test suite too).

This is the root cause of empty source code blocks on all example
pages: tokenize-sx uses set! in named let → empty tokens →
highlight returns "(<> )" → empty <code> blocks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:57:45 +00:00
9dd27a328b Add failing tests: highlight returns empty due to set! in named let
Root cause: set! inside named let loop does not persist mutations
to the outer scope in the WASM kernel. tokenize-sx uses this pattern
(set! acc (append acc ...)) inside (let loop () ...) and gets empty
tokens. This makes highlight return "(<> )" for all inputs, causing
empty source code blocks on every reactive island example page.

6 failing tests document the bug at each level:
- set! in named let loop (root cause)
- tokenize-sx (empty tokens)
- highlight (empty output)
- component-source + highlight (end-to-end)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:53:15 +00:00
28273eb740 Fix component-source SSR override, add SX island tests
component-source from data/helpers.sx was overriding the native
OCaml version. The SX version calls env-get with wrong arity (1 arg
vs required 2), producing empty source. Re-bind the native version
in SSR overrides after file loading.

Note: source code still not visible because highlight function
returns empty — separate issue in the aser rendering pipeline.

Also adds:
- spec/tests/test-reactive-islands.sx — 22 SX-native tests for all
  14 reactive island demos (render + signal logic + DOM)
- tests/node/run-sx-tests.js — Node runner for SX test files
- tests/node/test-reactive-islands.js — 39 Node/happy-dom tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:44:11 +00:00