### What's changed in v4.6.0
* feat: add coherent application build and dev lifecycle (#213) (by @patrickleet)
##### Summary
This PR makes Distributed application development a project command instead of a lifecycle-configuration exercise:
```bash
cd my-application
distributed build
distributed dev
```
The CLI discovers the Cargo workspace, typed application export, runtime binary, and conventional `ui/` SvelteKit project. Application authors do not create `distributed.contracts.json`, `distributed.lifecycle.json`, executor lists, readiness commands, or lifecycle shell scripts. The generated state belongs to the tool under `.distributed/lifecycle/`.
This also keeps the framework modular. Projects can continue using only Distributed's lower-level primitives. The build/dev experience in this PR applies when a project composes those pieces into a typed `ApplicationManifest`; a SvelteKit UI is optional.
##### How it works
```mermaid
sequenceDiagram
actor Author
participant Build as distributed build + Vite
participant Kit as SvelteKit
participant API as GraphQL gateway
participant Domain as Command handler + aggregate
participant Projector as Projection worker
participant DB as Read-model database
participant Replica as Browser replica
rect rgb(245, 247, 255)
Note over Author,Replica: Generate one application from authored Rust and page GraphQL
Author->>Build: Domain crates: commands, events, command RBAC
Author->>Build: Read-model crates: query shapes, relationships, read RBAC
Author->>Build: Projections: event → internal read-model mutation
Author->>Build: +page.graphql: @load and optional @live
Build-->>API: Command + query/subscription surfaces with RBAC
Build-->>Projector: Server projection programs
Build-->>Kit: Loaders, live operations, typed commands, replica plans
Build-->>Replica: Optimistic projection programs + declared Rust/WASM pures
end
rect rgb(245, 255, 247)
Note over Kit,Replica: Initial page render — @load
Kit->>API: Authorized @load query (SSR or navigation)
API->>DB: Read the authorized model slice
DB-->>API: Rows + identities + revisions
API-->>Kit: Query result
Kit->>Replica: Normalize, dehydrate, hydrate + server authority
Note right of Replica: Browser does not repeat the first query
Replica-->>Kit: Reactive confirmed snapshot
end
rect rgb(255, 251, 240)
Note over Kit,Replica: Ongoing page updates — @live
Kit->>Replica: Generated operation attaches live automatically
Replica->>API: Subscribe with the same query + variables
end
rect rgb(255, 245, 250)
Note over Kit,Replica: Write path — public domain command, never public model mutation
Kit->>Replica: Generated typed command + input
Replica-->>Kit: Apply predicted projection immediately
Replica->>API: Authorized domain command
API->>Domain: Execute command
Domain-->>Projector: Commit domain event
Projector->>DB: Apply internal projection mutation
DB-->>API: Publish committed read-model change
API-->>Replica: @live records + causal clocks
Replica-->>Kit: Confirm or reconcile the optimistic snapshot
Note over Projector,Replica: The generated projection protocol drives both server updates and browser optimism
end
```
At the SvelteKit boundary, a co-located `+page.graphql` document enters generation with the typed Rust application. `@load` populates a request-local server replica and dehydrates its authorized route seed into the browser without a duplicate first request. `@live` attaches automatically from the generated operation and carries committed records plus causal clocks back to the browser replica. Domain commands can update that replica optimistically while the same projection protocol updates committed server read models.
##### Application model
The typed Rust composition preserves CQRS responsibilities instead of deriving one CRUD API from domain structs:
The manifest is normal typed Rust composition—not author-maintained lifecycle JSON. New scaffolds create the export and existing projects can compose it from their real modules and surfaces. Two checked-in examples now exercise the same CLI path:
- [e2e-ui `application_manifest`](https://github.com/hops-ops/distributed/blob/feat/application-lifecycle-build-dev/tests/e2e-ui/crates/service/src/modules/graphql.rs#L139) — conventional discovery with a SvelteKit `ui/`
- [e2e-celld `application_manifest`](https://github.com/hops-ops/distributed/blob/feat/application-lifecycle-build-dev/tests/e2e-celld/crates/graphql-service/src/modules/graphql.rs#L139) — explicit package/runtime metadata in a multi-crate, API-only project
| Authored source | API/runtime responsibility |
|---|---|
| Domain aggregates and handlers | Command APIs and command RBAC; handlers execute domain commands and emit domain events |
| Read-model structs | GraphQL query/subscription surfaces and read RBAC |
| Projections | Domain-event → internal read-model mutation programs, applied to committed server records and browser replica slices |
| Declared Rust/WASM pure functions | Required browser artifacts compiled from their declaring Cargo package for optimism that cannot be predicted from command inputs alone |
| SvelteKit `+page.graphql` documents | Generated `@load` SSR/navigation loaders and `@live` subscriptions over the same authorized read-model operation |
| Service/application composition | Chooses which modules run together as one service or several, leaving the CAP trade-offs explicit |
GraphQL mutation syntax is internal projection IR here; it is not permission for public clients to mutate domain models directly. Public writes remain domain commands. Vite uses the typed application surface to generate the client and optimistic-replica artifacts.
##### What an author sees
###### Build the current project
```bash
$ cd tests/e2e-ui
$ distributed build
distributed build: compiling Rust runtime e2e-ui (e2e-runner)
distributed build: validating typed application e2e-service through e2e_service::application_manifest
distributed: compiling required browser WASM blob/pkg/blob_wasm from Cargo package blob-domain
distributed build: compiling SvelteKit UI .../tests/e2e-ui/ui
distributed build: introspecting typed application e2e-service
distributed build: project=e2e-ui application=e2e-service runtime=e2e-ui ui=ui
lifecycle graph: ok generation=sha256:... release=sha256:... nodes=1
```
From another directory, the project is a positional argument:
```bash
distributed build ./tests/e2e-ui
distributed build ./tests/e2e-ui --check --output json
```
`distributed build`:
1. reads the workspace model with `cargo metadata`;
2. resolves the typed application and runtime from scaffold-owned Cargo metadata or unambiguous conventions;
3. compiles the Rust runtime;
4. validates the real typed `ApplicationManifest` before starting the UI build;
5. compiles every declared browser WASM pure from its declaring Cargo package;
6. installs missing UI dependencies and runs the SvelteKit/Vite build when `ui/package.json` exists;
7. reuses the cached introspection harness and atomically activates an immutable, content-addressed application generation only after every program build succeeds.
Rust binaries remain Cargo outputs and SvelteKit uses its adapter-selected output. Lifecycle receipts, active-generation state, and the generated application manifest are internal CLI state under `.distributed/lifecycle/`.
`--check` rebuilds typed application metadata in isolation, compares it with the active generated manifest, emits drift ownership in JSON when requested, and does not activate or rewrite anything.
###### Run the current project
```bash
$ distributed dev
distributed dev: project=e2e-ui api=e2e-ui ui=ui (Ctrl-C to stop)
lifecycle dev: process api ready http://127.0.0.1:8791
lifecycle dev: process ui ready http://localhost:5180
lifecycle dev: ready generation=sha256:... processes=api,ui (Ctrl-C to stop)
```
`distributed dev`:
- compiles every declared browser WASM pure before Vite starts, and rebuilds it after relevant Rust changes;
- activates the initial typed application generation before serving;
- starts `cargo run` for the discovered runtime and `npm run dev` for `ui/`;
- uses bounded, framework-neutral TCP readiness checks and prints usable URLs;
- loads `<project-name>.env` and `.env` while preserving explicit shell environment values;
- leaves Svelte/CSS/module hot updates to Vite;
- watches typed Rust application inputs and restarts the API after a successful replacement generation;
- terminates both process groups and their descendants on Ctrl-C, including bounded TERM/KILL escalation.
Defaults are `BIND=127.0.0.1:8791`, `UI_HOST=localhost`, and `UI_PORT=5180`. Projects can override those normally through their shell or dotenv file.
##### Benefits and the features that provide them
| Benefit | Feature |
|---|---|
| A new contributor can build or run a project without learning internal lifecycle files | Cargo metadata + project convention discovery |
| Rust remains the semantic source of truth | Typed `ApplicationManifest` introspection; no Rust source scan or duplicated JSON inventory |
| Command and query responsibilities do not collapse into CRUD | Domain-derived commands; read-model-derived queries/subscriptions; projection-derived replica changes |
| A failed program build or manifest generation cannot advance active application metadata | Success barrier plus immutable generation activation |
| Declared browser pures require no app-owned build scripts | `portable_command!` records the declaring Cargo package; build/dev run `wasm-pack` before Vite |
| Frontend development keeps native HMR speed | Vite owns UI HMR; lifecycle supervision restarts only the Rust runtime for typed application changes |
| Local auth/database settings work without sourcing a script on every run | Project dotenv loading with shell precedence |
| Startup failures are understandable and fail before avoidable work | Typed-export preflight before Vite, visible Cargo/Vite output, explicit build phases, bounded compiler errors, and readiness URLs |
| Ctrl-C does not leave Cargo, Vite, or readiness descendants behind | Process-group supervision and bounded shutdown |
| CI tests the interface users invoke | Focused compiled-binary Bats coverage plus real e2e-ui and celld workflows entering through `distributed build` and `distributed dev` |
##### Scaffold and compatibility
New `distributed scaffold` projects receive tool-owned Cargo metadata identifying their application entrypoint and runtime binary. Existing workspaces need no metadata when they have one conventional `*-service` library exporting `application_manifest` and one non-manifest runtime binary.
The older file-driven lifecycle adapter remains available behind hidden `--root`, `--catalog`, and `--config` flags for compatibility and low-level graph tests. It is no longer the application-author workflow.
The same internal command prefixing supports the embedded `hops service build` / `hops service dev` surface as well as the standalone binary.
##### Real e2e UI
The checked-in e2e project now contains no lifecycle catalog, lifecycle config, placeholder manifest, or lifecycle preparation script.
```bash
cd tests/e2e-ui
make up # Postgres + Zitadel + e2e-ui.env
make ui-install # once: build + install this checkout's locally linked JS package
distributed build
distributed dev
```
Open:
- UI: `http://localhost:5180`
- GraphQL: `http://127.0.0.1:8791/graphql`
This fixture links `../../../js` so CI tests the exact JavaScript framework source in the checkout; `@hops-ops/distributed` is also published to npm for ordinary applications. The link is repository test setup, not application lifecycle configuration. Declared Rust/WASM pures are required framework artifacts: `distributed build` and `distributed dev` discover their declaring Cargo packages and compile them before Vite, with no `make wasm` or application-owned build script.
##### CI and verification
The integration workflow installs pinned Bats 1.14.0 and runs the compiled CLI. The suite passes locally:
```text
1..4
ok 1 project build and dev are zero-config and invalid typed exports fail before UI
ok 2 build activates atomically and check reports drift without replacing active
ok 3 dev reports process readiness, rebuilds selectively, and cleans descendants
ok 4 Ctrl-C cancels the initial build before any process starts
```
The integration workflows now prove the user-facing commands against two real application shapes:
| CI scenario | Entry point | What it proves |
|---|---|---|
| e2e-ui offline | `distributed build .` | Compiles the real runtime, validates/introspects its typed manifest, compiles the declared Blob Rust/WASM pure, builds the SvelteKit adapter output, and then runs domain, suite, generated-client drift, type, and UI tests |
| e2e-ui browser | `distributed build .` → `distributed dev .` | Supervises the real API and Vite processes; Playwright exercises auth, generated GraphQL `@load`/`@live`, commands, optimism, and browser behavior through their readiness URLs |
| e2e-celld live | `distributed build tests/e2e-ui` + `distributed build tests/e2e-celld` + `distributed dev tests/e2e-celld` | Discovers explicit metadata in a nested multi-crate workspace, builds/serves an API-only project, and exercises it against celld, Queue relay, NATS, and the separately owned shared UI |
The celld UI remains a separate Vite process because it physically belongs to `tests/e2e-ui`, not the API-only `tests/e2e-celld` project. This is an intentional optional-UI scenario, not a lifecycle bypass for the celld application.
Additional verification:
- e2e-celld `application_manifest_compiles_real_modules_and_surfaces` and its full GraphQL service suite — 10/10 passed
- real `distributed build tests/e2e-celld --check --output json` — `ok: true`, no drift
- `cargo test -p distributed_cli` — 215 unit tests plus all non-ignored CLI integration suites passed; lifecycle integration 9/9
- `cargo clippy -p distributed_cli --all-targets -- -D warnings` — passed
- real `distributed build tests/e2e-ui` — typed preflight, Rust, required `blob-domain` WASM, and SvelteKit/Vite builds passed with no lifecycle JSON; the activated manifest records `blob-domain` automatically
- incompatible checkout reproduction — missing `application_manifest` failed before Vite, named the exact package/export contract, and preserved the active generation
- real `distributed build tests/e2e-ui --check --output json` — `ok: true`, no drift
- e2e `application_manifest_compiles_real_modules_and_surfaces` — passed
- `git diff --check` — passed
All prior CodeRabbit findings were individually audited, acknowledged as valid, fixed with regressions, replied to, and resolved. The review-driven fixes cover initial-build cancellation, bounded file enumeration, root-derived/cross-platform locking, submitted-snapshot correctness, unpredictable test roots, bounded live stderr, typed cancellation outcomes, generated-output glob dependencies, process-group cleanup, and bounded readiness probes.
##### Linked work
GitKB tasks `coherent-build-dev-2`, `coherent-build-dev-3`, and `coherent-build-dev-4`. No GitHub issue was provided.
See full diff: [v4.5.0...v4.6.0](https://github.com/hops-ops/distributed/compare/v4.5.0...v4.6.0)