Commit Graph

3876 Commits

Author SHA1 Message Date
90190346aa host: relations-as-posts slice 3 — typed relations (target-type constraint enforced)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 46s
A relation's declares-anchor IS its target-type constraint: is-a/subtype-of (anchored
by type) require a type object; tagged (anchored by tag) a tag; related (no anchor) any
post. host/blog--valid-object?(kind, other) = other ∈ the relation's candidate pool — the
SAME set the picker offers — and relate-submit now enforces it (invalid target = silent
no-op). The picker never offers an invalid target, so this guards crafted/API requests:
the jump from candidate set to an enforced relation schema. A new typed relation needs
only a relation-post + a '<TargetType> declares <rel>' edge.

host/blog-relate! (direct/seed) stays unvalidated — validation is a handler boundary
(the seed writes 'X is-a relation', and relation isn't under type).

conformance 291/291 (+4: valid-object? accepts types/tags/any, relate-submit creates the
edge for a type object and no-ops for a non-type).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 07:25:49 +00:00
9c148e58dc plan: note the live serving-JIT iteration gotcha (Slice 2)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 30s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 23:17:04 +00:00
97f07cf40f host: rel-kinds is a boot-populated VALUE, loads unrolled (live JIT iteration bug)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 48s
The serving-mode JIT dropped 3 of 4 relations when host/blog-rel-kinds map/for-each'd
a function-produced list (only the first survived) — so only one relation editor
rendered live. Restore slice 1's working shape: host/blog-rel-kinds is a VALUE the
boot populates (set! in load-rel-kinds!), and both the cache loads and the list build
are UNROLLED (no iteration over the relation list). Metadata still lives on the
relation-posts. conformance 287/287.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 23:15:12 +00:00
a9df9f4e99 host: relation enumeration via a static slug list (graph scan was fragile on live)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 39s
host/blog-in "relation" "is-a" (a reduce over ALL edges) returned a partial set on
the live store (many edges), so only one relation editor rendered. Enumerate the
relations from a fixed slug list instead — deterministic; the metadata still lives
on the relation-posts (loaded into the cache). rel-kinds maps kind-spec over the
list and drops any uncached.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 23:03:37 +00:00
c6627f4954 host: relations-as-posts slice 2 — relation metadata lives on relation-posts
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 48s
is-a/subtype-of/tagged/related are now POSTS (each is-a a new `relation` root),
owning their metadata in a :rel slot {:symmetric :label :inverse-label}. The static
host/blog-rel-kinds registry is gone: kind-spec/rel-kinds/kind-symmetric? read the
relation-posts (via an in-memory cache), and the relation list derives from
host/blog-in "relation" "is-a".

Perform-budget fixes (a durable read inside the http-listen render VM raises
VmSuspended; too many per request 500s the page):
 - relation metadata is loaded into a cache at boot (host/blog-load-rel-kinds!,
   like load-edges!), so kind-spec is pure on render paths;
 - the initial edit page renders its pickers EMPTY (the load trigger fills each) —
   only the relate/unrelate FRAGMENT server-renders candidates (with-cands flag).
   Previously every edit page render did candidate-get × 4 pickers and 500'd.

host conformance 287/287 (+4 slice-2: kind-spec reads :rel, kind-symmetric? off the
post, unknown kind has no spec, rel-kinds derived from the graph). run-picker-check
3/3 (edit page boots, relate/unrelate flow works, no client errors).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:49:59 +00:00
b3804ce712 host: relations-as-posts slice 1 — declaration-driven candidate pools
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 42s
Types declare which relation they anchor (type declares is-a/subtype-of, tag
declares tagged) via a 'declares' edge; the picker's candidate set is the
down-closure of a relation's anchors through is-a ∪ subtype-of. So is-a/subtype-of
now offer the WHOLE type closure — the roots (type/tag/article) AND instances —
fixing the wrinkle where only instances showed and you could never pick 'tag' or
'article' as a type. 'related' has no anchor → every post.

Replaces the hardcoded :candidates "types"/"tags"/"all" with graph queries
(host/blog--reach-down + the declares edges). Design + roadmap (relations as
first-class posts, typed relations, type algebra, constraints) in
plans/relations-as-posts.md.

host conformance 283/283 (+5: is-a pool includes type roots, excludes plain posts,
tagged anchored by tag, related = all, is-a relate-options offers Article).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 21:40:27 +00:00
ad556c3e31 host: persistent Home link in the top nav
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 43s
Add a top nav with a boosted Home link, inside the [sx-boost] wrapper but outside
#content, so it SPA-navigates to / and survives every content swap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 21:17:43 +00:00
339235a2b5 host: no flash on relate/unrelate — server-render the picker's first page
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 34s
Relating/removing re-renders the kind's editor (outerHTML); the swapped-in picker's
results <ul> was empty and only filled after its 'load' fetch, so the candidate list
briefly emptied (a visible flash). Render the first page of candidates INTO the
results <ul> server-side (host/blog--relation-editor builds it inline via cons, the
same splice pattern the current-relations list uses), so the re-rendered picker
arrives already populated; the 'load' trigger then re-fetches the same page and
morphs it in place — invisible. No empty state, no flash.

Rendered inline rather than via the ~relate-picker component because component args
are evaluated, so pre-built candidate li-trees can't be spliced through one (they'd
be applied as calls). The component is left in place but unused.

Server-side only — the client engine (orchestration.sxbc, last commit's re-bind fix)
is unchanged. host conformance 278/278 (new: editor server-renders candidates), web
engine suite 8/8, run-picker-check 3/3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 20:50:40 +00:00
268e91cd5d host: relate/unrelate keep both lists in sync (add to current list, never blank the picker)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 30s
Two reported bugs on the edit page's relation editor:
 1. relating a candidate didn't add it to the current-relations list (the AJAX
    relate just deleted the candidate row; the relation only showed after a reload);
 2. removing a relation could blank the relate picker.

Fix (lib/host/blog.sx): both the candidate's relate form and a current relation's
remove form now target #rel-editor-<kind> with sx-swap=outerHTML, and the
relate/unrelate handlers return the re-rendered editor for that kind (current list +
a fresh picker). So one swap keeps BOTH lists in sync: the related post moves into
the current list and out of the (re-loaded) candidate pool; removing moves it back.
Gated on the SX-Target header, so a plain boosted form / no-JS POST (the is-a-tag
toggle) still redirects + re-renders #content.

Engine fix (web/orchestration.sx): handle-html-response's non-select branch called
post-swap on the OLD target, which an outerHTML swap has already REPLACED — so the
swapped-in content's triggers (here the re-rendered picker's "load") never bound and
the picker stayed empty. post-swap the swap result (the new node), mirroring the
sx-select branch. Recompiled orchestration.sxbc for the content-addressed client.

Tests:
 - web/tests/test-relate-picker.sx: relating re-syncs the editor (post in current
   list + picker re-loads); removing does likewise — both fail without the engine fix.
 - lib/host/tests/blog.sx: relate/unrelate return the re-rendered editor fragment
   (200, #rel-editor + picker), forms wire to #rel-editor-KIND/outerHTML, plain
   boosted POST still 303.
 - relate-picker.spec.js: the full in-page flow (relate adds to list, remove keeps
   the picker, no reload) + persistence.

Verified: host conformance 277/277, web engine suite 8/8, run-picker-check 3/3,
run-spa-check 3/3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 19:53:20 +00:00
09465f4483 host: removing a related post no longer clears the relate picker
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 34s
Bug: the edit page's remove button (on a current relation) was a plain boosted
form — POST /unrelate -> 303 redirect -> the engine re-rendered #content, and the
freshly-swapped relate picker came back EMPTY ("the list of posts to relate" was
cleared).

Fix: make the remove button an AJAX in-place delete, exactly like the relate
candidate rows — each current-relation <li> gets an id and its form carries
sx-post + sx-target=#cur-<kind>-<other> + sx-swap=delete. unrelate-submit returns
an empty 200 for that request so the engine deletes just that one row; #content is
never re-rendered, so the picker is untouched. method+action stay for no-JS.

The empty-200 is gated on the SX-Target header (sent only by the sx-post form), so
a plain boosted form / no-JS POST still redirects + re-renders — the is-a-tag
toggle and graceful degradation are unaffected.

Tests (all red before the fix):
 - lib/host/playwright/relate-picker.spec.js: the remove-button test now asserts
   the picker still has candidates after a removal (the reproduction).
 - web/tests/test-relate-picker.sx: an SX engine test — removing a current relation
   deletes just that row and leaves the sibling picker's list intact.
 - lib/host/tests/blog.sx: the relation-editor renders the AJAX delete attrs;
   unrelate returns empty-200 with SX-Target and 303 without.

Verified: host conformance 275/275, web engine suite 8/8, run-picker-check 2/2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 19:15:11 +00:00
53de29158b plan: Phase 3 render-to-console done; live TTY input loop noted as future
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 18:09:15 +00:00
16f90ffdad host/tests: Phase 3 — the engine renders the picker to a CONSOLE (non-browser target)
web/console-render.sx: render-to-console walks a live DOM element tree through the
engine's own dom-* accessors and prints it as terminal text — the results <ul>
becomes a bulleted list, the filter <input> a text field, the load-more sentinel a
"…" line, an .sx-error element a flagged line. It's the console platform's draw
step: the browser PAINTS the engine's tree, the harness ASSERTS it, this PRINTS it
— one tree, three bindings, the proof the engine is a general runtime not a browser
library.

Wired into the picker's SX engine tests (web/tests/test-relate-picker.sx): the load
and error tests now ALSO assert their console rendering — the same tree the engine
built drives both the DOM assertion and the terminal output, so Phase 1's suite is
the console renderer's regression suite for free. Plus a relate-picker:console suite
for the field/bullet/sentinel/error shapes. 7/7 green, no web-suite regressions.

(Class membership reads the live classList via dom-has-class?, not the static class
attribute — the engine adds .sx-error through classList.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 18:08:51 +00:00
2b2073cf56 plan: record Phases 0-2 done (SX engine tests + Playwright trim); Phase 3 (console) remains
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 17:56:35 +00:00
98ff7a350a host/tests: Phase 2 — trim Playwright to a boot smoke
The picker's per-behaviour browser tests are now SX engine tests
(web/tests/test-relate-picker.sx) + SX conformance (lib/host/tests/blog.sx), so
delete them from Playwright and keep only what needs a real boosted-SPA browser:

  spa-check.spec.js (3): WASM kernel boots + loads modules CONTENT-ADDRESSED
    (/sx/h/{hash} fetches, zero path-.sxbc fallback — new assertion) + marks
    ready; a boosted nav fragment-swaps #content (raw! HTML path); back/re-boost.
  relate-picker.spec.js (2): the bind-boost-form remove button; the picker
    re-binds its load trigger on content brought in by a boosted SPA nav.

Net: 11 browser tests -> 5. Both ephemeral-host suites verified green
(run-spa-check.sh 3/3, run-picker-check.sh 2/2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 17:56:07 +00:00
fe2da2d358 host/tests: Phase 1 — picker load/filter/paging/error-retry as SX engine tests
Port the rest of the relate-picker's interactive behaviours from Playwright into
the SX harness, driving the real engine against the mock DOM:
 - load: the form's "load" trigger populates the results on first render
 - filter: a debounced "input" re-fetches and narrows the candidates
 - paging: revealing the load-more sentinel pages in the next page (outerHTML
   swap replaces the sentinel)
 - error-retry: a dropped fetch marks .sx-error, and the next request clears it

Models two browser natives the OCaml runner lacks: observe-intersection (a
recording stub the test fires to simulate the sentinel scrolling into view) and
the synchronous-timer retry (stripped in the error test — backoff timing is a
test-engine.sx concern; here we assert the visible state).

Mock-DOM completeness (run_tests.ml): firstChild/lastChild on elements, so
children-to-fragment can drain a parsed fragment into an innerHTML/outerHTML swap
target. (Also repairs one pre-existing web test that needed firstChild.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 17:50:49 +00:00
297bdc6096 host/tests: Phase 0 — relate→delete row as an SX engine test (no browser)
Port the relate-picker's relate-delete behaviour from Playwright into an SX
harness test that drives the real engine (web/engine.sx + web/orchestration.sx)
against the OCaml runner's in-memory mock DOM. Builds the candidate row, runs
process-elements to bind the form's submit, mocks fetch-request to return the
host's empty 200, fires submit, and asserts the row is deleted in place — the
full fetch→swap→DOM-mutation loop in pure SX.

Mock-DOM completeness (run_tests.ml): NodeList.item(i) so dom-query-all can
iterate querySelectorAll results, and a DOMParser mock so the empty-body
sx-swap=delete path (handle-html-response → parseFromString) works as in a
browser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 17:40:02 +00:00
b0c0fdd4b1 plan: Phase 3 target is the CONSOLE — engine renders the same picker to a terminal
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 30s
The non-browser platform is a console/TUI renderer: the engine's platform ops map
to a text-node tree (harness-web's mock DOM is ~90% there), render-to-console
prints it, a raw-stdin input loop drives simulate-click/input. The same
~relate-picker runs unchanged in a terminal — browser is one platform binding,
console another, test harness a third.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 17:11:51 +00:00
0b13701ea4 plan: SX-native engine tests (browser-independent) — port picker behaviors to the SX harness, trim Playwright to a boot smoke
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 29s
Durable plan for the next step: drive the engine against the mock platform
(spec/harness.sx :fetch + web/harness-web.sx simulate-click/DOM asserts), so
fetch->swap->DOM behavior is tested without a browser — the same engine could
drive a non-browser target. Phases: PoC (relate-delete), port the rest, trim
Playwright to WASM-boot + content-addressed-load, stretch = non-browser renderer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 17:09:11 +00:00
f1bd6f1557 engine: boosted forms now submit (bind-boost-form was discarding method/action)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 40s
Fixes "the remove button does nothing — no network, no console". A plain form on
a boosted (sx-boost) page has no sx-get/sx-post, so the SPA engine boosts it and
binds submit -> execute-request. But bind-boost-form called
`(execute-request form nil nil)` — discarding the method+action it was handed —
and execute-request then asks get-verb-info for a verb, gets nil, and no-ops. So
EVERY plain boosted form silently did nothing: the related-posts "remove" button,
the editor Save button, the is-a-tag toggle.

Fix: pass the form's own method+action as the verbInfo
`(dict "method" method "url" action)`, so the request actually fires (body built
from the form fields). A latent web-engine bug surfaced by the host's edit page —
the first page with plain boosted POST forms.

Test: relate-picker.spec.js gains a remove-button case (relate, reload, click
remove, assert the relation is gone) — 7/7. WASM rebuilt (boot-helpers.sxbc).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 17:07:07 +00:00
c0007740e7 host: relate removes just the picked candidate row in place (no reload)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 33s
Picking a candidate to relate it no longer does a full POST -> 303 -> reload.
The candidate <li> now carries an id and its relate form is an AJAX sx-post
(sx-target="#cand-<kind>-<other>", sx-swap="delete"): on success the engine
deletes just that one row — the item is now related, so it leaves the candidate
pool with no reload and no candidate-list refetch. host/blog-relate-submit returns
an empty 200 for an SX request (so the delete swap fires) and still 303s for a
plain POST (no-JS fallback via the form's method+action).

relate-picker.spec.js test 4 updated to assert the in-place row delete + no reload
+ the relation still persists (shows on the post page). 6/6 + conformance 272/272.

(Symmetric unrelate-in-place was prototyped but backed out: the current-links
form, bound via boot's process-elements rather than post-swap, didn't fire the
AJAX delete despite identical markup — a binding quirk to chase separately. Unrelate
keeps its plain POST -> reload for now, no regression.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 15:49:03 +00:00
b21ae05e8f host: extract the relate picker into a content-addressed ~relate-picker component
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 34s
The declarative picker markup is now a reusable SX component
(lib/host/sx/relate-picker.sx, defcomp ~relate-picker &key slug kind) instead of
inline markup in the editor. It is a CONTENT-ADDRESSED, CLIENT-EXPANDED component:

- Server: on a full page load render-page expands ~relate-picker server-side
  (SEO / no-JS), exactly as before.
- Client: on a boosted SPA nav the edit body serialises to the compact
  (~relate-picker :slug … :kind …), and the CLIENT expands it. The component
  module is compiled to a content-addressed .sxbc, served immutably from
  /sx/h/{hash}, and listed in the page's data-sx-manifest "boot" array so the
  client eager-loads it after the web stack — registering its defcomp before any
  boosted fragment references it.

Wiring:
- lib/host/sx/relate-picker.sx — the component.
- lib/host/blog.sx — editor emits (~relate-picker :slug s :kind k); the inline
  form markup is gone.
- lib/host/static.sx — host/static-manifest-json emits boot:["relate-picker.sxbc"]
  (the previously-empty boot array, now used as designed).
- hosts/ocaml/browser/sx-platform.js — loadWebStack eager-loads the page manifest's
  boot[] modules (content-addressed) after the web stack.
- bundle.sh + compile-modules.js — copy/compile the component to .sxbc.
- serve.sh + conformance.sh — load the component module server-side.

This gives the host an app-component system: app defcomps shipped to the client by
hash, the same machinery as the kernel modules — the picker is the first, and it's
the model for publishing components externally.

Tests: conformance 272/272 (server expansion); relate-picker.spec.js 6/6 incl. the
boosted-nav populate (proves client-side component load + expansion) and the
error/retry case. WASM stack rebuilt (relate-picker.sxbc @ 6818110a).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 15:17:30 +00:00
db4809b01e host/engine: visible error/retry state for failed fetches + retry on network failure
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 30s
Two engine fixes in web/orchestration.sx (rebuilt into the WASM bytecode) plus the
blog CSS that surfaces them.

1. Retry on NETWORK failure, not just HTTP errors. The fetch error/catch path (the
   real offline / DNS / connection-refused case) previously dispatched
   sx:requestError and stopped — only a non-ok HTTP response with an empty body
   ever reached handle-retry. So "no connection" never recovered. Now the catch
   path calls handle-retry too, so an sx-retry element actually self-heals when the
   connection returns (the cap bounds the backoff interval, not the attempt count —
   it retries forever).

2. Visible failure state. On any failed/aborted fetch the engine adds an `.sx-error`
   class to the element (cleared, with the retry backoff reset, on the next
   success). Without it a stuck retry loop is invisible — the picker just sits
   "Loading…". The blog shell ships CSS so the relate picker shows "Connection
   problem — retrying…" / "offline, retrying…" on .sx-error.

Platform-wide: any sx-get/sx-post element benefits, not just the picker.

Tests: relate-picker.spec.js gains a 6th case — abort relate-options, assert
.sx-error appears, un-abort, assert it clears and the picker repopulates (proving
the retry loop is live). 6/6 browser + 272/272 conformance. WASM web stack rebuilt
(orchestration.sxbc + the static hs-* copies refreshed by the same build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 14:48:35 +00:00
bdc7e02fbc host: content-addressed SPA cache + declarative SX-htmx relate picker + SIGPIPE hardening
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 37s
Three composing pieces that make the blog SPA correct and resilient.

Content-addressed module cache (lib/host/static.sx, serve.sh, blog.sx shell,
conformance.sh): index each web-stack .sxbc by the content hash in its head,
serve GET /sx/h/{hash} immutable text/sx, and emit <script data-sx-manifest>
{file->hash} so the WASM client loads modules content-addressed (localStorage +
immutable) instead of path + max-age. serve.sh builds the index at boot;
conformance.sh now loads static.sx before blog.sx (the shell calls
host/static-manifest-json).

Declarative relate picker (lib/host/blog.sx, lib/dream/form.sx): replace the
inline /relate-picker.js blob — which never ran on swapped-in content, so the
candidate list was empty after a boosted nav to /<slug>/edit — with a declarative
SX-htmx form: sx-get relate-options on "load" + debounced "input", innerHTML-swap
the results ul; infinite scroll via a server-emitted "load more" sentinel
(sx-trigger revealed, sx-swap outerHTML) that pages the rest, q preserved via a
new symmetric dr/url-encode. The engine re-binds these triggers on swapped
content, so the picker populates on full load AND boosted SPA nav. Candidate
relate forms get :sx-disable (plain POST->303->reload, their original behavior;
the engine would otherwise boost them and swap the redirect unreliably).
sx-retry "exponential:1000:30000" on the form+sentinel retries a dropped/offline
fetch forever (the cap bounds the interval, not the attempts).

SIGPIPE hardening (hosts/ocaml/bin/sx_server.ml): the native http-listen server
had no SIGPIPE handler, so a client aborting an in-flight fetch (the engine
cancels superseded requests on a debounced filter/fast nav) closed the socket
mid-write and killed the whole process (exit 141). Ignore SIGPIPE so the failed
write becomes a catchable Sys_error the per-connection handler already swallows.

Tests: host conformance 272/272; relate-picker.spec.js 5/5 incl. a boosted-nav
populate regression; spa-check 4/4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 14:30:17 +00:00
b9a24d5870 web: re-boost swapped content from the [sx-boost] ancestor (fixes back-then-click full reload)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 33s
After a fragment swap, process-elements(target) -> process-boosted(target) only
boosted [sx-boost] containers that are DESCENDANTS of the swap target. But the
swap target (#content) is nested UNDER the boost wrapper (<div sx-boost="#content">
<div id="content">), so re-boosting scoped to the target found nothing — the
swapped-in links never got bound. Only the initial document-wide boot boost
worked, so: home->sub worked (home links boosted at boot), but Back restored the
home content unboosted, and the next click did a full page reload. (Post-page
links were unboosted too; Back just exposed it.)

process-boosted now ALSO boosts from the nearest [sx-boost] ANCESTOR of root
(dom-closest), so any swap target inside a boost scope gets its links rebound.
is-processed? guards keep it idempotent.

spa-check: the back-button test now clicks AGAIN after Back and asserts it's a
SPA nav (no full reload) — would have caught this. .sxbc regenerated.

Verified: spa-check 4/4 (incl. click-after-back).
2026-06-29 13:41:50 +00:00
f5b6612ee1 web+host: fix raw! HTML dropped in client SX render (dom-parse-html returned a NodeList)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 41s
dom-parse-html returned body.childNodes — a NodeList, not a Node — so the client
SX render did appendChild(NodeList) and threw "Argument 1 does not implement
interface Node", silently dropping every raw! HTML block (e.g. a post's <article>
body). It surfaced only now because the blog renders fragments client-side
(text/sx) since this session; before, fragments were server HTML so sx-render
never ran on raw!. The error is caught/non-fatal, and the spa-check suite only
asserted the footer + URL behaviour, so it passed through a dropped post body.

- dom-parse-html now returns a DocumentFragment (moves the parsed nodes in): a
  real Node, appendChild-able as one unit, and queryable — which also fixes the
  already-broken hs-htmx callers that did (dom-query doc ...) / (dom-first-child
  doc) on what was a NodeList.
- spa-check: assert #content article is visible after a boosted nav, so a dropped
  post body fails the suite (closes the test gap).
- .sxbc regenerated; bundle dom.sx synced to canonical web/lib/dom.sx.

Verified: spa-check 4/4 (incl. the new article assertion).
2026-06-29 13:27:13 +00:00
59ac51a8ba kernel+bundle: fix nil-bytecode .sxbc (compile-blob serializer dropped Integer)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 46s
Every .sxbc shipped with `:bytecode (nil nil ...)` and `:arity nil`, so the WASM
kernel's vm.sx hit "VM: unknown opcode 0" on every module and fell back to .sx
source (slower, noisy console). Root cause: `raw_serialize` in the `compile-blob`
command (sx_server.ml) handles `Number` but not `Integer`, and bytecode opcodes +
arity/upvalue-count are `Integer`s — so they fell through to the `_ -> "nil"`
catch-all and serialized as nil. Same class of bug as the value_to_js Integer gap
(689dae7d). It went unnoticed because source-fallback masks it. Add the Integer
case and regenerate: the web stack now loads entirely from bytecode (0 unknown-
opcode warnings, 0 source fallbacks), boost + SPA unchanged. compiler.sx in the
bundle was also stale — re-synced to the canonical lib/compiler.sx.

Verified: native host conformance 271/271; chromium boots with 0 unknown-opcode
warnings + 0 source-fallback loads; spa-check still passes (boost 6/6, fragment
swap). Prereq for content-addressing the assets (caching real bytecode, not nil).
2026-06-29 13:17:20 +00:00
41f3e9b276 host: SPA fragments are SX wire format (text/sx), rendered client-side by the WASM kernel
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 34s
Boosted (SPA) requests now return the SX source of the content (serialize) with
content-type text/sx, so the engine's handle-sx-response parses + sx-renders it
client-side on the WASM OCaml kernel — instead of server-rendered HTML. Direct /
no-JS requests still get the full HTML shell (SEO + first paint).

- host/blog--page: fragment branch serializes the body tree to SX wire format
  (was render-page -> HTML); full branch unchanged (HTML shell).
- host/blog--resp: new content-type-aware wrapper (text/sx for boosted, text/html
  otherwise); replaced the 13 dream-html/dream-html-status call-site wrappers.
- listings built with (cons (quote ul) items) not (list (quote ul) items): the
  list form nests children as one list and relied on render-to-html flattening
  it; sx-render (client) treats (li ...) as a call -> 'Not callable'. cons splices
  them into canonical (ul li1 li2 ...) that renders identically on both sides.

Verified: native host conformance 271/271; SX-Request returns text/sx SX source,
direct request text/html; lib/host/playwright/spa-check 4/4 (boot, boost, SX
fragment swap, back button) in chromium.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 13:03:48 +00:00
059897970e host: doc — blog SPA complete + live on the WASM OCaml kernel
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 27s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:11:18 +00:00
689dae7d0c host+kernel: blog SPA boost works end-to-end on the WASM OCaml kernel (Playwright 4/4)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 43s
Clicking a blog link now fragment-swaps #content with URL push + working back
button, no full reload — the SX-htmx engine driving the same OCaml kernel the
server runs. Six bugs in the source-load + boost path, found by bisecting in
chromium, all fixed:

1. Import double-apply (sx_server.ml x2, sx_browser.ml): the import suspension
   handlers computed `key = library_name_key lib_spec` then called
   `library_loaded_p key` — but library_loaded_p applies library_name_key
   itself, so it ran sx_to_list on a string and crashed ("Expected list, got
   string"). Only unloaded libs suspend, so it only bit lazy imports. Pass the
   spec, not the key.

2. Unloaded-import crash (spec/evaluator.sx + sx_ref.ml library_exports): an
   import of a not-yet-loaded library returned nil exports, and bind-import-set
   did (keys nil) -> crash. Return an empty dict so the import is a graceful
   no-op (lazy symbol resolution covers real usage).

3. value_to_js missing Integer (sx_browser.ml): integers passed to host methods
   were mishandled, so dom-query-all's (host-call node-list "item" i) ignored i
   and returned node 0 for every index — every element aliased the first, so
   only one link ever boosted. Add the Integer -> JS number case.

4. browser-same-origin? rejected relative URLs (browser.sx x2): it only did
   (starts-with? url origin), so "/alpha/" was treated as cross-origin and
   should-boost-link? refused every relative link. Accept scheme-less,
   non-protocol-relative URLs.

5. dom-query-in undefined (orchestration.sx x2): the swap path called a function
   that exists nowhere; it's just dom-query with a container arg.

6. Lazy-deps never loaded under source fallback (sx-platform.js): lazy symbol
   resolution only fires on the VM GLOBAL_GET path, but source-loaded swap
   callbacks run on the CEK and raise instead of lazy-loading, so the post-swap
   hs-boot-subtree!/htmx-boot-subtree! were undefined and aborted URL push.
   Preload the manifest's lazy-deps.

Verified: native host conformance 271/271; lib/host/playwright/spa-check 4/4
(boot, boost, fragment swap + URL push, back button) in real chromium against an
ephemeral durable host server.
2026-06-29 11:09:11 +00:00
05c0a0b01a host: doc — complete boost diagnosis (nil .sxbc bytecode + manifest-mapped lib resolution)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 22s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 09:01:12 +00:00
0ca70eb4b5 host: doc — wasm kernel boot crash fixed (crypto), boost still blocked on web-stack load
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 41s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 07:57:18 +00:00
fce9e0c617 kernel: make the crypto/content-addressing stack actually WASM-safe (32-bit ints)
The kernel's sha2/cbor/cid/ed25519 modules were labelled 'WASM-safe' but assumed
63-bit native int. On the web targets — js_of_ocaml (32-bit int) and
wasm_of_ocaml (31-bit int) — they truncated, producing wrong digests/CIDs and a
Char.chr crash at kernel INIT (ed25519 precomputes sqrtm1 + base_point at module
load, driving the base-2^26 bignum). This is why a freshly-built browser kernel
crashed on boot while the stale committed artifact (older toolchain) still ran.

Fixes (all verified bit-identical to the 63-bit native build, conformance 271/271):
- sx_sha2: SHA-256 round words via Int32 (were native int + land 0xFFFFFFFF,
  which is a no-op on 31-bit and overflows the constants); both SHA-256/512
  length-encoding via Int64 shifts (native "lsr 32" is shift-mod-32 on js, which
  leaked the length byte into a higher word). NIST vectors pass native/js/wasm.
- sx_cbor: write_head width selection + byte emission via Int64 (the 0x100000000
  literal truncated to 0 on js, sending small ints to the 8-byte branch; and
  "v lsr (8*i)" with i>=4 was shift-mod-32).
- sx_cid: base32_lower keeps acc bounded to the unconsumed low bits (it grew 8
  bits/byte and overflowed). cid_from_sx now matches native<->js exactly.
- sx_ed25519: bignum mul accumulates in Int64 (26x26=52-bit products overflow);
  div_small running remainder in Int64 (rem<<26 ~= 2^34). This was the boot gate
  — the browser kernel now boots (SxKernel live, crypto-sha256 correct on js).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 07:51:50 +00:00
4df4de7f79 host: doc — SPA WASM bundle rebuild attempt failed (Char.chr crash), reverted
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 21:00:25 +00:00
dbcbc39ebe host: blog SPA scaffolding (WASM kernel) — server side complete, boost blocked on bundle rebuild
Turn the blog into a SPA using the SX-htmx engine (web/engine.sx) booting the
WASM OCaml kernel (same evaluator as the server) in-browser, with sx-boost
fragment-swapping every link into #content.

Server side DONE + verified:
- lib/host/static.sx: GET /static/** serves shared/static via the file-read
  primitive (ctype by ext, traversal-guarded, 404 on missing). Wired into
  serve.sh (module + route group). Tested: kernel JS + .wasm binary-exact.
- host/blog--page is now the SPA shell: full page = WASM boot scripts +
  sx-boost=#content wrapper + #content; on SX-Request:true returns ONLY the
  inner content fragment for the engine to swap. All 13 handlers thread req.
- docker-compose mounts ./shared/static.
- lib/host/playwright/spa-check.{spec.js,run-spa-check.sh}: boot/boost/swap/back.

Client side: the WASM kernel BOOTS (SxKernel object, data-sx-ready=true, web
stack loads). BLOCKER: the bundled .sxbc throw 'VM: unknown opcode 0' vs this
worktree's kernel -> .sx source fallback -> boot.sx source fails 'Expected
list, got string' -> process-boosted never binds links (boosted 0/N). Fix =
rebuild a consistent WASM bundle (recompile .sxbc against the kernel via
scripts/sx-build-all.sh); the browser wasm target isn't built here yet. See
plans/host-spa.md. Live NOT redeployed (stays on pre-SPA process).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 20:53:06 +00:00
d8d7663565 host: fix serving-JIT host miscompile — install IO resolver for http-listen
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 33s
The serving-JIT perform-in-HO-callback miscompile (map/rest/drop wrong
CALL_PRIM args → blank pages, empty picker) is now fully fixed, so the host
runs 100% serving JIT with NO jit-exclude.

sx-vm-extensions 81177d0e resolves a suspended HO-callback's IO inline
(instead of unwinding the native map/filter loop and corrupting the stack),
but ONLY when a synchronous resolver is installed (!_cek_io_resolver = Some).
The host serves via the http-listen primitive, whose handler drove durable IO
through cek_run_with_io with the resolver = None — so it hit the unwinding
path the fix doesn't cover. (The vm-ext repro installed a resolver, so it
never exercised the host's real no-resolver path.)

Fix: extract cek_run_with_io's IO resolution into resolve_io_request, and have
http-listen install _cek_io_resolver := Some (fun req _ -> resolve_io_request
req) — byte-identical resolution, so the inline path resolves durable reads
exactly as the CEK loop would.

Verified: host conformance 271/271; ephemeral durable server at 100% JIT (no
exclude) zero fallbacks + real content + related shown + picker 12 candidates;
live blog.rose-ash.com home/post/tags 200 with related posts, zero error-log
lines; relate-picker Playwright 4/4 (infinite-scroll + filter + relate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 20:13:24 +00:00
8104aadc2b Merge branch 'loops/sx-vm-extensions' into loops/host 2026-06-28 19:54:09 +00:00
81177d0ebd vm-ext: fix serving-JIT perform-in-HO-callback miscompile (host bug)
Root cause (found via bin/repro_jit_resume.ml, 9 surgical cases): when a
`perform` (durable kv read) fires inside a native HO-primitive callback
(map/filter/reduce/for-each/some/every?), the VmSuspended unwound through
the primitive's native OCaml loop (List.map etc.), destroying the loop's
iteration state. The remaining elements were dropped and the stack left
misaligned, so the NEXT CALL_PRIM (map/rest/drop) read wrong args —
"map: expected (fn list)", "rest: 1 list arg", "drop: list and number".
Only triggers in the http-listen + cek_run_with_io serving path (epoch
eval has no synchronous resolver, so conformance was 271/271).

(A) lib/sx_vm.ml call_closure_reuse: when a callback suspends AND a
synchronous IO resolver is installed (serving mode), resolve the
callback's IO inline and run it to completion right there, returning its
value to the native loop — so the loop is never unwound. Scoped to the
resolver-set path; the CEK-driven path (flow/reactive/async tests) keeps
its existing reuse_stack behaviour, so nothing else changes. reuse_stack
is isolated across the nested resume.

(A') lib/sx_vm.ml resume_vm: re-assert _active_vm := Some vm for the
duration of the resumed run (mirrors call_closure). call_closure restored
_active_vm to the caller when VmSuspended unwound, so HO callbacks during
a resume could land on the wrong VM. Latent-bug fix.

(B) bin/sx_server.ml register_jit_hook: the resolve_loop runs inside the
VmSuspended handler, so a non-VmSuspended exception from resume_vm escaped
to the http handler (→ 500). Catch it and fall back to CEK for THIS call
(mark jit_failed, return None → interpreter re-runs it). Self-heals on the
first hit, not a retry. Defense-in-depth; with (A) it shouldn't trigger.

Verification: repro 9/9 (incl. host shape: map[cb→interpreted-helper
perform]→drop = (7 8); reduce; nested map). Standard + --full OCaml
conformance unchanged at 4834/1110 (baseline identical — the 1110 are
pre-existing environmental: host-call-fn/browser-platform symbols,
rational display, tw/regex). Host loop to re-verify 271/271 serving and
drop its (jit-exclude! "host/*" "dream-*" "dr/*") band-aid.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 19:44:12 +00:00
83eaa12393 host: restore host jit-exclude — 100% JIT silently CORRUPTS output
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 36s
The 100%-JIT experiment surfaced something worse than the 500s: the kernel
miscompile also returns WRONG RESULTS with no error — blank pages (render map
yields empty) and an empty relate picker (drop in relate-options yields []).
Conformance (CEK) passes these, so the code is correct; the JIT silently
produces garbage. Silent corruption is worse than a crash, so the request path
runs on CEK again (IO-bound — no perf loss). Datalog/relations JIT stays on
(/tags 0.16s). Restoring it brought back content + the 17-candidate picker.
Go 100% JIT again once sx-vm-extensions fixes the OP_PERFORM-resume bug.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 19:31:39 +00:00
a697904c7c docs: refined serving-JIT miscompile data (3 fns, list-prim-after-perform)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 26s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 19:16:32 +00:00
7172f0d775 host: 100% serving JIT — drop the host jit-exclude (surface miscompiles in dev)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 27s
Per dev intent: don't mask JIT errors. Remove (jit-exclude! "host/*" "dream-*"
"dr/*") so ALL request-path SX runs under JIT. Host handlers miscompile on first
call in the http-listen path (map/rest arg bug → 500, self-heals on retry); that
surfacing is the point — it exercises the JIT against real durable-IO traffic and
gives the sx-vm-extensions loop the full miscompile list to fix (kernel bugs A/B
in plans/HANDOFF-jit-miscompile.md). Datalog JIT win stays (/tags fast).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 19:13:54 +00:00
1c487ebe0e docs: hand off serving-JIT host miscompile to sx-vm-extensions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 19:09:26 +00:00
389cf96838 host: enable serving-mode JIT (SX_SERVING_JIT=1), host code excluded
The Datalog/relations saturation is CPU-bound and JITs cleanly: host conformance
271/271 under JIT, 5.4x faster (1m43s -> 19s, same binary); live /tags 2.5s ->
0.76s. loops/host now carries the merged sx-vm-extensions kernel (the JIT engine
+ gate), built into the binary the container bind-mounts.

- docker-compose: SX_SERVING_JIT=1 (default-OFF gate; opt-in here).
- serve.sh: when JIT is on, (jit-exclude! "host/*" "dream-*" "dr/*"). The host app
  + Dream framework MISCOMPILE on first call in the http-listen + cek_run_with_io
  path (map/rest emit wrong CALL_PRIM args -> 500; the JIT->CEK fallback marks the
  fn failed but does NOT recover the failed call). They're IO-bound, so CEK is no
  slower — but the miscompile is a real kernel-JIT bug to fix upstream (see
  plans/HANDOFF-jit-miscompile.md), after which this exclude can be dropped.

Verified live: cold pages 200 (no first-hit 500), relate picker lists candidates,
relate round-trip works, /tags fast, datalog still JITs (78 dl-* compiles).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 19:08:22 +00:00
17c7b90834 Merge branch 'loops/sx-vm-extensions' into scratch/host-jit
# Conflicts:
#	hosts/ocaml/bin/sx_server.ml
#	lib/erlang/runtime.sx
2026-06-28 18:57:17 +00:00
e6a1180d50 docs: serving-JIT handoff (from sx-vm-extensions) + host-loop correction
Carry the sx-vm-extensions loop's serving-JIT handoff notes, and add a
correction: the post-page slowness was the durable read count (fixed in
0a2f1a61), not the (long-gone) Smalltalk render path — so SX_SERVING_JIT is an
optional general speedup, not the perf blocker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 18:53:25 +00:00
0a2f1a61d1 host: typed relations — Phase 6 (schema validation) + post-page perf fix
Phase 6 — gradual schema validation made real:
- host/blog-type-schemas now carries a declarative schema (a list of
  {:block :msg} required-element rules); "article" requires an h1.
- host/blog--all-tags / --schema-issues / host/blog-type-issues walk the parsed
  content and report each missing required block; host/blog-type-valid? = no
  issues. A type with no schema imposes nothing (gradual).
- seed an "article" type-post (article subtype-of type). edit-submit now lists
  the specific schema issues on a 400 ("an article needs a heading"), so a post
  that is-a article must satisfy it on save.

Post-page performance (the unresponsiveness): a post page was ~1s even with no
relations and no load — NOT CPU (render-page ~2ms, in-memory handler ~5ms) but
the DURABLE read path: host/blog--relation-blocks called host/blog-out/in, each
re-scanning the whole KV (host/blog-slugs + an all-edges scan), so a page did ~7
kv-keys performs deep in the call stack. Each durable perform routes through
cek_run_with_io and is costly there. Fixes:
- host/blog-out/in read DIRECT edges from the durable edge store (string scan),
  not lib/relations (whose queries re-saturate the Datalog ruleset, ~seconds).
- host/blog--relation-blocks reads the KV key list ONCE and derives both the post
  set and the edges in memory (host/blog--edges-for / --recs-slugs), one kv-keys
  plus a host/blog-get per linked post. Post pages: ~1s -> ~0.02s (46x); live
  11-135s -> ~0.15s. lib/relations stays for TRANSITIVE queries only.
- conformance timeout 300 -> 600s: the relations-heavy blog suite is CPU-bound
  under shared-box contention and was tripping a false truncation at 300.

271/271 (blog 100). Verified live: post pages fast, Tags/Related/Tagged-with-this
render, schema rejection works.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 18:52:56 +00:00
7e50d3d1bb host: typed relations — Phase 4 cleanup, registry-driven render + /tags
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 21s
Replace the hard-coded related/tagged blocks with iteration over the registry,
so adding a kind renders automatically — no handler edit.

- host/blog--relation-blocks: iterates host/blog-rel-kinds; each kind contributes
  its outgoing block (label) and, if it has an inverse, its incoming block
  (inverse-label, e.g. tagged -> "Tagged with this", is-a -> "Instances"). Empty
  blocks dropped; one kv-keys read up front, relation lookups in-memory.
  host/blog--relations-or-hint adds the logged-in "add some" hint when empty.
- host/blog--relation-editors: one editor per registry kind on the edit page
  (Related / Types / Subtype of / Tags), replacing the hard-coded two.
- GET /tags: index of every tag (a post that is-a tag), each linking its own page.
- dropped host/blog--related-block / --kind-block / --tagged-with-block (folded
  into host/blog--edges-block + the registry iteration).
- GOTCHA (4th time): host/blog-tags-index called host/blog-get INSIDE the item
  quasiquote -> VmSuspended/500 live (conformance in-memory store can't see it);
  pre-fetch records before the quasiquote.

5 tests (relations-section hint, registry render of Related+Tags, inverse block
for a tag, /tags lists + 200). 265/265; Playwright 4/4. Verified live: /tags,
post pages show registry blocks, tag page shows Types + Tagged-with-this, edit
page has a picker per kind.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 17:29:58 +00:00
62b7fc1ff0 host: typed relations — Phase 3, tags as posts
A tag is just a post that is-a tag; tagging is a "tagged" edge to it. End to end:
mark a post a tag, tag posts with it, see a post's tags and a tag's members.

- helpers: host/blog-is-tag? (= is-a? slug "tag"), host/blog-tags (out tagged),
  host/blog-tagged-with (in tagged), host/blog-instances-of (a type's members,
  O(#subtypes) not O(#posts) — the efficient candidate source).
- picker generalised to be KIND-AWARE and MULTI-INSTANCE: relate-options takes
  &kind=, candidates come from the kind's registry :candidates (all/tags/types);
  /relate-picker.js wires every .relate-picker box by data-kind (a Related picker
  and a Tags picker now coexist on the edit page).
- render: post page gains a "Tags" block; a tag post additionally lists "Tagged
  with this" (its members). edit page: a Related editor + a Tags editor + an
  "is this post a tag" toggle (reuses /relate kind=is-a — no new route).
- GOTCHA (again): host/blog--relation-editor read host/blog-out INSIDE its
  quasiquote -> VmSuspended/500 under http-listen + durable edges; moved the read
  to a let before the quasiquote (conformance can't see it — in-memory store;
  the ephemeral Playwright run caught it).

6 conformance tests (is-tag?, instances-of, tag+tagged-with, tagged picker offers
only tags, related picker still all, is-a-tag toggle) -> 261/261. Playwright
multi-picker 4/4. Verified live: ocaml made a tag, welcome tagged ocaml, Tags
block + Tagged-with-this both render.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 17:09:53 +00:00
cb2fc788d7 host: typed relations — Phase 2, type resolution with subsumption
The spine: types ARE posts, and typing is transitive the right way. is-a
(instance-of) does NOT chain on its own, but subsumption does — an instance of a
subtype is an instance of the supertype.

- registry gains "subtype-of" (directed, transitive). host/blog-types-of(slug) =
  declared is-a targets PLUS every subtype-of-ancestor of each (composed host-side
  over relations/descendants — no new Datalog rules). host/blog-is-a?(slug,type)
  is transitive through subtype-of.
- host/blog-seed-types! seeds the root type-posts "type" and "tag" (real posts
  that document themselves) with tag subtype-of type, so anything is-a tag is
  transitively a type. Idempotent; wired into serve.sh.
- gradual-validation seam: host/blog-type-schemas (empty) + host/blog-schema-of +
  host/blog-type-valid? (vacuously true with no schemas) wired into edit-submit
  alongside the parse check — enforcement is a one-line add later, not a retrofit.

6 tests: types-of = declared + all subtype-of supertypes; is-a? transitive
through subtype-of; is-a alone does NOT chain; instance of tag is transitively a
type; type-valid vacuous with no schemas. 255/255.

Verified live: /type/ + /tag/ render as posts, tag subtype-of type survived a
recreate (durable), ocaml is-a tag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 16:49:00 +00:00
fed58b2814 vm-ext: exclude js parser (jp-*) from JIT — fixes js 147/148 -> 148/148
The lone js opt-in-JIT residual was async/await_in_loop, which failed to PARSE
under JIT ("Unexpected token: op '<'" on `i < 5`) while passing on CEK. The js
exclusion was "js-*", but the recursive-descent parser is the jp-* namespace
(75 functions in lib/js/parser.sx) — only the lexer/transpile/runtime are js-*.
So the parser was left JIT-eligible and a jp-* function miscompiled this
construct (the long-standing parser-miscompile class).

Fix: extend the js exclusion to "js-* jp-*" so the parser is interpret-only too,
matching how every other guest's front-end is handled. js conformance under
SX_SERVING_JIT=1 is now 148/148, == CEK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 16:42:27 +00:00
b0b0a0592b host: durable lazy sessions — logins survive a restart
Sessions were in-memory, so a restart logged everyone out (same class as the
relation wipe). Move them to the durable store, but LAZILY so anonymous/crawler
traffic doesn't spam it: session/create mints a sid with no row; the row appears
on the first session/set (a login). A per-boot epoch (one durable write at
startup, host/session-init!) keeps sids unique across restarts without a write
per request.

- lib/host/session.sx: lazy backend (create = no row, set = create row,
  exists = row written) + epoch/in-memory-counter sid generation.
- serve.sh: point the session store at the durable backend + host/session-init!.
- blog.sx: host/current-principal is now a durable read, so host/auth-footer
  (home + post footers) had to move OUT of the quasiquote into let bindings —
  a perform during page-tree build raises VmSuspended (the whole site 500'd for
  a beat). Principal computed once per page.
- 2 session tests: create writes no row, set creates the row.

249/249. Verified live: site renders (anon + authed), login + footer survive a
container force-recreate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 16:37:26 +00:00