Files
rose-ash/CLAUDE.md
giles 2ee4d4324a Document VM debugging tools and island authoring rules in CLAUDE.md
Tools: vm-trace, bytecode-inspect, deps-check, prim-check,
test_boot.sh, sx-build-all.sh — with usage examples.

Island rules: (do ...) for multi-expression bodies, nested let for
cross-references, (deref (computed ...)) for reactive derived text,
effects in inner let for OCaml SSR compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 01:32:55 +00:00

15 KiB

Rose Ash Monorepo

Cooperative web platform: federated content, commerce, events, and media processing. Each domain runs as an independent Quart microservice with its own database, communicating via HMAC-signed internal HTTP and ActivityPub events.

Deployment

  • Do NOT push until explicitly told to. Pushes reload code to dev automatically.
  • NEVER push to main — pushing to main triggers a PRODUCTION deploy. Only push to main when the user explicitly requests a production deploy. Work on the macros branch by default; merge to main only with explicit permission.

Project Structure

blog/           # Content management, Ghost CMS sync, navigation, WYSIWYG editor
market/         # Product catalog, marketplace pages, web scraping
cart/           # Shopping cart CRUD, checkout (delegates order creation to orders)
events/         # Calendar & event management, ticketing
federation/     # ActivityPub social hub, user profiles
account/        # OAuth2 authorization server, user dashboard, membership
orders/         # Order history, SumUp payment/webhook handling, reconciliation
relations/      # (internal) Cross-domain parent/child relationship tracking
likes/          # (internal) Unified like/favourite tracking across domains
shared/         # Shared library: models, infrastructure, templates, static assets
artdag/         # Art DAG — media processing engine (separate codebase, see below)

Shared Library (shared/)

shared/
  models/          # Canonical SQLAlchemy ORM models for all domains
  db/              # Async session management, per-domain DB support, alembic helpers
  infrastructure/  # App factory, OAuth, ActivityPub, fragments, internal auth, Jinja
  services/        # Domain service implementations + DI registry
  contracts/       # DTOs and service protocols
  browser/         # Middleware, Redis caching, CSRF, error handlers
  events/          # Activity bus + background processor (AP-shaped events)
  config/          # YAML config loading (frozen/readonly)
  static/          # Shared CSS, JS, images
  templates/       # Base HTML layouts, partials (inherited by all apps)

Art DAG (artdag/)

Federated content-addressed DAG execution engine for distributed media processing.

artdag/
  core/      # DAG engine (artdag package) — nodes, effects, analysis, planning
  l1/        # L1 Celery rendering server (FastAPI + Celery + Redis + PostgreSQL)
  l2/        # L2 ActivityPub registry (FastAPI + PostgreSQL)
  common/    # Shared templates, middleware, models (artdag_common package)
  client/    # CLI client
  test/      # Integration & e2e tests

SX Language — Canonical Reference

The SX language is defined by a self-hosting specification in shared/sx/ref/. Read these files for authoritative SX semantics — they supersede any implementation detail in sx.js or Python evaluators.

Specification files

  • shared/sx/ref/eval.sx — Core evaluator: types, trampoline (TCO), eval-expr dispatch, special forms (if, when, cond, case, let, and, or, lambda, define, defcomp, defmacro, quasiquote), higher-order forms (map, filter, reduce, some, every?, for-each), macro expansion, function/lambda/component calling.
  • shared/sx/ref/parser.sx — Tokenizer and parser: grammar, string escapes, dict literals {:key val}, quote sugar (`, ,, ,@), serializer.
  • shared/sx/ref/primitives.sx — All ~80 built-in pure functions: arithmetic, comparison, predicates, string ops, collection ops, dict ops, format helpers, CSSX style primitives.
  • shared/sx/ref/render.sx — Three rendering modes: render-to-html (server HTML), render-to-sx/aser (SX wire format for client), render-to-dom (browser). HTML tag registry, void elements, boolean attrs.
  • shared/sx/ref/bootstrap_js.py — Transpiler: reads the .sx spec files and emits sx-ref.js.

Type system

number, string, boolean, nil, symbol, keyword, list, dict,
lambda, component, macro, thunk (TCO deferred eval)

Evaluation rules (from eval.sx)

  1. Literals (number, string, boolean, nil) — pass through
  2. Symbols — look up in env, then primitives, then true/false/nil, else error
  3. Keywords — evaluate to their string name
  4. Dicts — evaluate all values recursively
  5. Lists — dispatch on head:
    • Special forms (if, when, cond, case, let, lambda, define, defcomp, defmacro, quote, quasiquote, begin/do, set!, ->)
    • Higher-order forms (map, filter, reduce, some, every?, for-each, map-indexed)
    • Macros — expand then re-evaluate
    • Function calls — evaluate head and args, then: native callable → apply, lambda → bind params + TCO thunk, component → parse keyword args + bind params + TCO thunk

Component calling convention

(defcomp ~card (&key title subtitle &rest children)
  (div :class "card"
    (h2 title)
    (when subtitle (p subtitle))
    children))
  • &key params are keyword arguments: (~card :title "Hi" :subtitle "Sub")
  • &rest children captures positional args as children
  • Component body evaluated in merged env: closure + caller-env + bound-params

Rendering modes (from render.sx)

Mode Function Expands components? Output
HTML render-to-html Yes (recursive) HTML string
SX wire aser No — serializes (~name ...) SX source text
DOM render-to-dom Yes (recursive) DOM nodes

The aser (async-serialize) mode evaluates control flow and function calls but serializes HTML tags and component calls as SX source — the client renders them. This is the wire format for HTMX-like responses.

Platform interface

Each target (JS, Python) must provide: type inspection (type-of), constructors (make-lambda, make-component, make-macro, make-thunk), accessors, environment operations (env-has?, env-get, env-set!, env-extend, env-merge), and DOM/HTML rendering primitives.

Tech Stack

Web platform: Python 3.11+, Quart (async Flask), SQLAlchemy (asyncpg), Jinja2, HTMX, PostgreSQL, Redis, Docker Swarm, Hypercorn.

Art DAG: FastAPI, Celery, JAX (CPU/GPU), IPFS/Kubo, Pydantic.

Key Commands

Development

./dev.sh                        # Start all services + infra (db, redis, pgbouncer)
./dev.sh blog market            # Start specific services + infra
./dev.sh --build blog           # Rebuild image then start
./dev.sh down                   # Stop everything
./dev.sh logs blog              # Tail service logs

Deployment

./deploy.sh                     # Auto-detect changed apps, build + push + restart
./deploy.sh blog market         # Deploy specific apps
./deploy.sh --all               # Deploy everything

Art DAG

cd artdag/l1 && pytest tests/              # L1 unit tests
cd artdag/core && pytest tests/            # Core unit tests
cd artdag/test && python run.py            # Full integration pipeline
cd artdag/l1 && ruff check .               # Lint
cd artdag/l1 && mypy app/types.py app/routers/recipes.py tests/

Architecture Patterns

Web Platform

  • App factory: create_base_app(name, context_fn, before_request_fns, domain_services_fn) in shared/infrastructure/factory.py — creates Quart app with DB, Redis, CSRF, OAuth, AP, session management
  • Blueprint pattern: Each blueprint exposes register() -> Blueprint, handlers stored in _handlers dict
  • Per-service database: Each service has own PostgreSQL DB via PgBouncer; cross-domain data fetched via HTTP
  • Alembic per-service: Each service declares MODELS and TABLES in alembic/env.py, delegates to shared.db.alembic_env.run_alembic()
  • Inter-service reads: fetch_data(service, query, params) → GET /internal/data/{query} (HMAC-signed, 3s timeout)
  • Inter-service writes: call_action(service, action, payload) → POST /internal/actions/{action} (HMAC-signed, 5s timeout)
  • Inter-service AP inbox: send_internal_activity() → POST /internal/inbox (HMAC-signed, AP-shaped activities for cross-service writes)
  • Fragments: HTML fragments fetched cross-service via fetch_fragments() for composing shared UI (nav, cart mini, auth menu)
  • Soft deletes: Models use deleted_at column pattern
  • Context processors: Each app provides its own context_fn that assembles template context from local DB + cross-service fragments

Auth

  • Account is the OAuth2 authorization server; all other apps are OAuth clients
  • Per-app first-party session cookies (Safari ITP compatible), synchronized via device ID
  • Grant verification: apps check grant validity against account DB (cached in Redis)
  • Silent SSO: prompt=none OAuth flow for automatic cross-app login
  • ActivityPub: RSA signatures, per-app virtual actor projections sharing same keypair

SX Rendering Pipeline

The SX system renders component trees defined in s-expressions. Canonical semantics are in shared/sx/ref/ (see "SX Language" section above). The same AST can be evaluated in different modes depending on where the server/client rendering boundary is drawn:

  • render_to_html(name, **kw) — server-side, produces HTML. Maps to render-to-html in the spec.
  • render_to_sx(name, **kw) — server-side, produces SX wire format. Maps to aser in the spec. Component calls stay unexpanded.
  • render_to_sx_with_env(name, env, **kw) — server-side, expands known components then serializes as SX wire format. Used by layout components that need Python context.
  • sx_page(ctx, page_sx) — produces the full HTML shell (<!doctype html>...) with component definitions, CSS, and page SX inlined for client-side boot.

See the docstring in shared/sx/async_eval.py for the full evaluation modes table.

Service SX Directory Convention

Each service has two SX-related directories:

  • {service}/sx/ — service-specific component definitions (.sx files with defcomp). Loaded at startup by load_service_components(). These define layout components, reusable UI fragments, etc.
  • {service}/sxc/ — page definitions and Python rendering logic. Contains defpage definitions (client-routed pages) and the Python functions that compose headers, layouts, and page content.

Shared components live in shared/sx/templates/ and are loaded by load_shared_components() in the app factory.

Art DAG

  • 3-Phase Execution: Analyze → Plan → Execute (tasks in artdag/l1/tasks/)
  • Content-Addressed: All data identified by SHA3-256 hashes or IPFS CIDs
  • S-Expression Effects: Composable effect language in artdag/l1/sexp_effects/
  • Storage: Local filesystem, S3, or IPFS backends
  • L1 ↔ L2: scoped JWT tokens; L2: password + OAuth SSO

Domains

Service Public URL Dev Port
blog blog.rose-ash.com 8001
market market.rose-ash.com 8002
cart cart.rose-ash.com 8003
events events.rose-ash.com 8004
federation federation.rose-ash.com 8005
account account.rose-ash.com 8006
relations (internal only) 8008
likes (internal only) 8009
orders orders.rose-ash.com 8010

Dev Container Mounts

Dev bind mounts in docker-compose.dev.yml must mirror the Docker image's COPY paths. When adding a new directory to a service (e.g. {service}/sx/), add a corresponding volume mount (./service/sx:/app/sx) or the directory won't be visible inside the dev container. Hypercorn --reload watches for Python file changes; .sx file hot-reload is handled by reload_if_changed() in shared/sx/jinja_bridge.py.

Key Config Files

  • docker-compose.yml / docker-compose.dev.yml — service definitions, env vars, volumes
  • deploy.sh / dev.sh — deployment and development scripts
  • shared/infrastructure/factory.py — app factory (all services use this)
  • {service}/alembic/env.py — per-service migration config
  • _config/app-config.yaml — runtime YAML config (mounted into containers)

Tools

  • Use Context7 MCP for up-to-date library documentation
  • Playwright MCP is available for browser automation/testing

VM / Bytecode Debugging Tools

These are OCaml server commands sent via the epoch protocol (printf '(epoch N)\n(command args)\n' | sx_server.exe). They're available in any context where the OCaml kernel is running (dev server, CLI, tests).

# Full build pipeline — OCaml + JS browser + JS test + run tests
./scripts/sx-build-all.sh

# WASM boot test — verify sx_browser.bc.js loads in Node.js without a browser
bash hosts/ocaml/browser/test_boot.sh

(vm-trace "<sx-source>")

Step through bytecode execution. Returns a list of trace entries, each with:

  • :opcode — instruction name (CONST, CALL, JUMP_IF_FALSE, etc.)
  • :stack — top 5 values on the stack at that point
  • :depth — frame nesting depth

Requires the compiler to be loaded (lib/compiler.sx). Use this to debug unexpected VM behavior — it shows exactly what the bytecode does step by step.

printf '(epoch 1)\n(load "lib/compiler.sx")\n(epoch 2)\n(vm-trace "(+ 1 2)")\n' | sx_server.exe

(bytecode-inspect "<function-name>")

Disassemble a compiled function's bytecode. Returns a dict with:

  • :arity — number of parameters
  • :num_locals — stack frame size
  • :constants — constant pool (strings, numbers, symbols)
  • :bytecode — list of instructions, each with :offset, :opcode, :operands

Only works on functions that have been JIT-compiled (have a vm_closure). Use this to verify the compiler emits correct bytecode.

printf '(epoch 1)\n(bytecode-inspect "my-function")\n' | sx_server.exe

(deps-check "<sx-source>")

Strict symbol resolution checker. Parses the source, walks the AST, and checks every symbol reference against:

  • Environment bindings (defines, let bindings)
  • Primitive functions table
  • Special form names (if, when, cond, let, define, etc.)

Returns {:resolved (...) :unresolved (...)}. Run this on .sx files before compilation to catch typos and missing imports (e.g., extract-verb-info vs get-verb-info).

printf '(epoch 1)\n(deps-check "(defcomp ~my-comp () (div (frobnicate x)))")\n' | sx_server.exe
# => {:resolved ("defcomp" "div") :unresolved ("frobnicate" "x")}

(prim-check "<function-name>")

Scan compiled bytecode for CALL_PRIM instructions and verify each primitive name exists in the runtime. Returns {:valid (...) :invalid (...)}. Catches mismatches like length vs len that would crash at runtime.

printf '(epoch 1)\n(prim-check "my-compiled-fn")\n' | sx_server.exe
# => {:valid ("+" "len" "first") :invalid ("length")}

SX Island Authoring Rules

Key patterns discovered from the reactive runtime demos (see sx/sx/reactive-runtime.sx):

  1. Multi-expression bodies need (do ...)fn, let, and when bodies evaluate only the last expression. Wrap multiples in (do expr1 expr2 expr3).
  2. let is parallel, not sequential — bindings in the same let can't reference each other. Use nested let blocks when functions need to reference signals defined earlier.
  3. Reactive text needs (deref (computed ...)) — bare (len (deref items)) is NOT reactive. Wrap in (deref (computed (fn () (len (deref items))))).
  4. Effects go in inner let — signals in outer let, functions and effects in inner let. The OCaml SSR evaluator can't resolve outer let bindings from same-let lambdas.