giles 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
2026-03-25 00:36:57 +00:00
2026-02-24 20:10:23 +00:00

Rose Ash

Monorepo for the Rose Ash cooperative platform — six Quart microservices sharing a common infrastructure layer, a single PostgreSQL database, and an ActivityPub federation layer.

Services

Service URL Description
blog blog.rose-ash.com Content management, Ghost sync, navigation, editor
market market.rose-ash.com Product listings, scraping, market pages
cart cart.rose-ash.com Shopping cart, checkout, orders, SumUp payments
events events.rose-ash.com Calendar, event entries, container widgets
federation federation.rose-ash.com OAuth2 authorization server, ActivityPub hub, social features
account account.rose-ash.com User dashboard, newsletters, tickets, bookings

All services are Python 3.11 / Quart apps served by Hypercorn, deployed as a Docker Swarm stack.

Repository structure

rose-ash/
├── shared/              # Common code: models, services, infrastructure, templates
│   ├── models/          # Canonical SQLAlchemy ORM models (all domains)
│   ├── services/        # Domain service implementations + registry
│   ├── contracts/       # DTOs, protocols, widget contracts
│   ├── infrastructure/  # App factory, OAuth, ActivityPub, fragments, Jinja setup
│   ├── templates/       # Shared base templates and partials
│   ├── static/          # Shared CSS, JS, images
│   ├── editor/          # Prose editor (Node build, blog only)
│   └── alembic/         # Database migrations
├── blog/                # Blog app
├── market/              # Market app
├── cart/                # Cart app
├── events/              # Events app
├── federation/          # Federation app
├── account/             # Account app
├── docker-compose.yml   # Swarm stack definition
├── deploy.sh            # Local build + restart script
├── .gitea/workflows/    # CI: build changed apps + deploy
├── _config/             # Runtime config (app-config.yaml)
├── schema.sql           # Reference schema snapshot
└── .env                 # Environment variables (not committed)

Each app follows the same layout:

{app}/
├── app.py               # App entry point (creates Quart app)
├── path_setup.py        # Adds project root + app dir to sys.path
├── entrypoint.sh        # Container entrypoint (wait for DB, run migrations, start)
├── Dockerfile           # Build instructions (monorepo context)
├── bp/                  # Blueprints (routes, handlers)
│   └── fragments/       # Fragment endpoints for cross-app composition
├── models/              # Re-export stubs pointing to shared/models/
├── services/            # App-specific service wiring
├── templates/           # App-specific templates (override shared/)
└── config/              # App-specific config

Key architecture patterns

Shared models — All ORM models live in shared/models/. Each app's models/ directory contains thin re-export stubs. factory.py imports all six apps' models at startup so SQLAlchemy relationship references resolve across domains.

Service contracts — Apps communicate through typed protocols (shared/contracts/protocols.py) and frozen dataclass DTOs (shared/contracts/dtos.py), wired via a singleton registry (shared/services/registry.py). No direct HTTP calls between apps for domain logic.

Fragment composition — Apps expose HTML fragments at /internal/fragments/<type> for cross-app UI composition. The blog fetches cart, account, navigation, and event fragments to compose its pages. Fragments are cached in Redis with short TTLs.

OAuth SSO — Federation is the OAuth2 authorization server. All other apps are OAuth clients with per-app first-party session cookies (Safari ITP compatible). Login/callback/logout routes are auto-registered via shared/infrastructure/oauth.py.

ActivityPub — Each app has its own AP actor (virtual projection of the same keypair). The federation app is the social hub (timeline, compose, follow, notifications). Activities are emitted to ap_activities table and processed by EventProcessor.

Development

Quick deploy (skip CI)

# Rebuild + restart one app
./deploy.sh blog

# Rebuild + restart multiple apps
./deploy.sh blog market

# Rebuild all
./deploy.sh --all

# Auto-detect changes from git
./deploy.sh

Full stack deploy

source .env
docker stack deploy -c docker-compose.yml coop

Build a single app image

docker build -f blog/Dockerfile -t registry.rose-ash.com:5000/blog:latest .

Run migrations

Migrations run automatically on the blog service startup when RUN_MIGRATIONS=true is set (only blog runs migrations; all other apps skip them).

# Manual migration
docker exec -it $(docker ps -qf name=coop_blog) bash -c "cd shared && alembic upgrade head"

CI/CD

A single Gitea Actions workflow (.gitea/workflows/ci.yml) handles all six apps:

  1. Detects which files changed since the last deploy
  2. If shared/ or docker-compose.yml changed, rebuilds all apps
  3. Otherwise rebuilds only apps with changes (or missing images)
  4. Pushes images to the private registry
  5. Runs docker stack deploy to update the swarm

Required secrets

Secret Value
DEPLOY_SSH_KEY Private SSH key for root access to the deploy host
DEPLOY_HOST Hostname or IP of the deploy server

Infrastructure

  • Runtime: Python 3.11, Quart (async Flask), Hypercorn
  • Database: PostgreSQL 16 (shared by all apps)
  • Cache: Redis 7 (page cache, fragment cache, sessions)
  • Orchestration: Docker Swarm
  • Registry: registry.rose-ash.com:5000
  • CI: Gitea Actions
  • Reverse proxy: Caddy (external, not in this repo)
Description
No description provided
Readme 61 MiB
Languages
Python 58.8%
JavaScript 34%
OCaml 2.8%
HTML 1.7%
Common Lisp 1.5%
Other 1.2%