Scores own deploy. harmony_app is identity, images, context, and HarmonyApp→Scores. Authoring DX lives in dx. Compose/chart/Application model and the Helm render test are gone.
4.5 KiB
Architecture Decision Record: Application Components — One File, One Type
Initial Author: Jean-Gabriel Gill-Couture
Initial Date: 2026-09-03
Last Updated Date: 2026-09-03
Status
Proposed.
Supersedes the app-facing use of a single Application graph
(harmony_app::application) as how an application is structured.
Does not replace ADR-023 (framework Scores) or ADR-028 (typed Refs).
Context
An application is a map of components someone can point at. The crate
should be that map. Earlier shapes hid it: one god-object Application,
a type per runtime (FrontendCommand / FrontendContainer), both image
and command on one blob checked later, a typestate builder that encodes
bind order (a DAG — and real systems cycle, ADR-028).
Decision
One component, one file, one type. Opening frontend.rs is opening
the frontend.
Runtimes are capabilities on that type, not sibling types:
Container— image on a clusterCommand— a process (the command is a string in the impl)Remote— this context does not run it; it only names where it lives
A database that is always on the cluster implements Container only. A
frontend that is a container in one context and a process in another
implements both. The struct is still Frontend.
A context binds each component to one runtime that component
implements. That bind is an ordinary struct the author fills in
(frontend: app.frontend.as_command(), …) — missing a field or calling
as_command() on something with no Command impl does not compile. Not
a macro: macros are cryptic if Rust is not your first language. Refs whose content depends on how it runs (public origin vs
localhost) live on that runtime impl. Contexts are user-declared.
Harmony may ship default bindings as examples, not as a closed enum.
Components exchange typed Refs (ADR-028). A Ref is desired state, valid before either side has run. Cycles are two Refs, not two builder stages.
A new framework Score exists only when Harmony should orchestrate a
reusable capability. App glue is ordinary code in the component file.
Org-specific Scores live in that org's crate, not in harmony.
Planned framework Score: OCI/image — Dockerfile in config, build/push
from the context, export ContainerRef for K8sDeploymentScore. Until
then, build/push may be procedural and still yield a ContainerRef.
Principles
- Feldman: illegal states don't compile (
Commandon Postgres, Mailhog in Production,CalloutRefwhereIssuerRefis required). Runtime is not a boolean on a god object. - Crichton: file = mental object; you can reason about
frontend.rswithout the mesh. Notation matches:impl Command for Frontendis "we run it with a command". - Parse, don't validate: no
.image+.commandon one blob checked later. Missingimpl Commandis the parse. - SOLID: SRP = one file; ISP = Postgres doesn't see
Command; DIP = others depend on refs; OCP = a new context is new impls on context-dependent components only; LSP =FrontendstaysFrontend, refs stayUrlRef.
Ugly we accept: N named impl Production / LocalDev / Staging
on dual-runtime files. Better than a mesh-wide enum or a typestate DAG.
Ugly we refuse: a second type per runtime; context as if local
inside scores; builder order as types.
Harmony's job: Container, Command, Remote, Unit, scores, refs.
App's job: one file per component, impl the runtimes it actually has, impl the contexts that bind them.
A process launcher (devbox run, cargo watch, …) is a string inside
impl Command for Frontend, never a Harmony type.
Consequences
- The crate layout is the documentation — five components or tens.
- Always-cluster components keep a single
Containerimpl. - The
Applicationbuilder was deleted. Apps implementHarmonyAppand compose Scores. --context(ADR-026) selects bindings. It does not rename types.
Alternatives considered
Single Application declaration. One lowering to Scores. The mental
model sits behind a god object.
Typestate builder. Readable as a script; types encode order. Breaks cycles; Harmony would have to know the app's slots.
Dual types per runtime. The author no longer has one frontend.
Closed context enum in Harmony. Defaults become the product. Applications cannot declare their own tying-together.
Additional Notes
Related: ADR-023, ADR-026, ADR-028.
First implementation: harmony_app::dx (Command / Container /
Remote, as_command / as_container, Slot / Ref), HostProcessScore.