Imported from bitplorer/ux-compose (
AGENTS.md). Install upstream withnpx skills add bitplorer/ux-compose. Copyright stays with the author.
AGENTS.md — ux-compose
Orientation for humans and agents continuing this package.
First-time: START_HERE.md. Map: docs/INDEX.md.
Concern→file: docs/ARCHITECTURE.md. Do not add docs/MODULE_MAP.md.
Read docs/OWNERSHIP.md (ownership SSoT) then START_HERE.md
then docs/INDEX.md. Public names: src/ux_compose/__init__.py __all__.
Floor: Python ≥ 3.14. Hard-deps are the pinned specialists in
pyproject.toml ([project].dependencies): ux-dom, ux-channel, ux-behavior,
ux-motion, cek-host>=0.1.3, cek-surface>=0.1.3. Clone path:
pip install -e ".[dev]" / pip install -e ".[serve]". Create-app path:
pip install -r requirements.txt. Missing specialists fail loud.
Layer ownership (hard cut)
The UX stack is a layered system of specialists, not a monolith. Compose is allowed to look like “the product” to authors. It imports specialists and must not reimplement them.
| Layer | Owns | Must not own |
|---|---|---|
| ux-dom | HTML/CSS/JS trees, Document, serialize, pure discovery, uxdom, package static |
Intent, Cap, Result ops, MorphState, motion IR, product CLI, Tailwind compiler, app asset layout |
| ux-channel | Intent / Result / Cap / wire / peers / Cap Host (cek-runtime; ≠ HTTP Product host) | HTML trees, CSS |
| ux-behavior | Product behavior, Morph/Ref, @action, validation |
Raw HTML construction, wire codecs |
| ux-motion | Presence / transition plans as data (IR v1) | Product behavior, DOM construction |
| ux-compose (this repo) | Author composition + product CLI (uxcompose: create-app, build, serve, deploy, doctor) + Tailwind compiler + WebAssets layout |
Re-implementing Document serialize |
Do not invent a sixth product. ux-app is retired.
Folder law (do not "tidy" into drawers)
Folders are import/copy laws. Full table: docs/ARCHITECTURE.md Folder law.
kit/is the ownable catalog.uxcompose addrewritesux_compose.kit.X.kit_construct.pystays next tocomponent.pyso copies keep the library import. Do not move it underkit/.- No
cli/package.cli.pyis argv dispatch only. Verb bodies are also libraries (doctor.py,build.py/cli_build.py,scaffold.py,serve_dev.py).serve_dev.pystarts sibling CSS--watchand tunnel. Leftoverstart_css_watcher=is gone. - Frozen serve verbs:
dev/prod/restart-channel. argvdevelopment/production/restart_channelfail closed. Noserve//services/ghost packages. wire/is the onlyux_channeldoor.routing/is the host pair.- Do not add
helpers/or growdx/into a product.
Author-facing surface (do not invent names)
From __all__ (author verbs): App, Component, MorphState, RefState,
action, bind, control, notify, update_with, morph_play,
act, mark_dirty, field, status, optional_plan, optional_fade,
optional_slide, AttachNote, attach_notes, Level, doctor.
Host / surface / motion also on __all__: build, WebAssets,
DirectoryRoutes, DirectoryASGI, RouterHooks, Surface /
SurfaceBundle / SurfaceError / mount_surfaces / scan_surfaces /
validate_surfaces, DoctorResult, scene, fade, rise, slide,
HAS_DOM, and DOM tags (div, h1, button, …).
Kit is not a public import path for product apps. Own a copy with
uxcompose add. from ux_compose.kit import X is leftover.
There is no public ux.div / when / forall / Page on this package.
Do not document them. Tags are imported from ux_compose.
What not to invent
- Product CLI on
uxdom(create-app, productbuild,serve,deploy) - Tailwind compiler on ux-dom (
ux_compose.tailwind+uxcompose buildown it) - App asset layout /
WebAssetson ux-dom (ux_compose.assetsowns it) - HMR as a
Document.useproduct API - A file watcher,
HmrHub, or TailwindPopeninsidehmr.py - Clock flags (
--no-hmr,--no-reload,--css-watch). Modes choose clocks. - Process-reloading the worker because
input.csschanged - A second Tailwind
--watchnext to serve's sibling (two writers onoutput.css) - A single-uvicorn fallback next to origin + ui + channel
- Product code importing
ux_channeloutside composewire/ - A copy of Channel codecs, Document serialize, motion IR, or the StateStore protocol in this tree
- A
fragment.py(orhelpers/) home for the HTML walker._fragment_for_targetprefers ux-domextract_by_idwhen importable; homemade walker is escape if absent — leftover, not a second serialize - Dual product paths
- A second HTTP pipeline, FastAPI HTML
default_response_class,StreamingRoute, or HTTP verbs on page units (see Product host below) location.reload()as the happy path after a.pysave (morph first)
Dev clocks under uxcompose serve
Do not collapse these. The stale design is an in-process hub + watcher.
| Clock | Owner | Signal |
|---|---|---|
| Process reload | ui worker, uvicorn --reload on *.py |
new ui process, cold import |
| Browser live-reload | hmr.py WebSocket /__uxcompose/hmr |
ui death → GET 200 → morph; location.reload() on fail |
| CSS | tailwind.py sibling Tailwind --watch + client HEAD /css/output.css |
stylesheet swap. No process dies |
uxcompose serve dev is origin + ui + channel. Always.
uxcompose serve prod is one process, clocks off.
cli.py parses the mode; serve_dev.py starts the clocks (CSS watch +
tunnel). Missing extras fail closed — no single-uvicorn fallback.
HTML insert is HmrClientMiddleware, not Document.use.
assets.py _StaticDirASGI must emit ETag / Last-Modified.
Architecture: docs/internals/hmr.md.
Decision: docs/adr/0005-serve-dev-split.md.
How-to: docs/guides/serve-hmr-tunnel.md.
CLI spine
uxcompose create-app myapp --level 1
uxcompose serve dev
uxcompose build
uxcompose deploy --provider docker
uxcompose doctor .
Pure-dom: uxdom doctor | lint | profile | add.
Product CSS: uxcompose build (ux_compose.tailwind finds / ensures the CLI).
Tests
make test314
# or:
PYTHONPATH=src:. python -m pytest tests/ -q
See docs/guides/TESTING.md. Regression tests under tests/regression/
lock the hard-cut (no product CLI dual path).
Isolation
Cold import never pulls the wire. App.use_channel(asgi_app=…) is the live door.
app.use_motion() is the motion door. Level 1 code remains correct at L2/L3.
uxcompose serve dev session lives with Channel (ch.draft). Origin still
sends Document GET to the ui worker. When REDIS_URL is unset, compose
prepares one sqlite path (UXCOMPOSE_STATE_STORE); Channel.boot opens
Channel's FileStateStore. Channel prefers Redis when REDIS_URL is set
— do not export both. Do not reimplement that store in this tree. Redis
(REDIS_URL) is the product multi-worker path. Do not route HTML GET to
channel — that would drop HMR for route edits.
Cap door is Channel.boot (not ActionRegistry.from_config — that name
is not a frozen wire import). Pin: ux-channel @ a6ab159 (Python ≥3.14 tip;
Cut C floor 985e58a). Leftover
teaching for retired hooks lives on docs/ARCHITECTURE.md
Agent leftovers.
Product host (Clock A)
HTTP Product host (Clock A / ADR 0002) ≠ CEK Cap Host (cek-runtime via
wire/cek; channel ADR 0009 / 0010).
Read docs/reference/host.md before changing
routing/, build.py, scaffold.py, or wire/boot.py. Decision:
docs/adr/0002-product-host.md.
Do not invent a second HTTP pipeline. Payload type picks media type (dict →
JSON, generator → stream, tree/str → HTML). Do not set FastAPI
default_response_class. Do not use StreamingRoute. Do not put HTTP verbs
on page units. Do not boot Channel in App.boot("auto"). New media types
follow spec §10 (predicate + both hosts + tests/unit/test_host.py + the spec
page in the same change). build() wraps GET only with the author Document
(wrap=). attach_motion() returns instances, not classes.
App.mount / attach_page_router pass the same wrap= as build().
materialize(route_class=) fails closed. Scaffold does not emit page().
Examples (examples/live_asgi.py) use build() for Clock A GET — not a
handmade @app.get + HTMLResponse. App.mount is the catalog scan
(scan_surfaces). HTTP path law is DirectoryRoutes.discover. Two
walkers, one product door (build()). Do not merge them.