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.
115 lines
4.5 KiB
Markdown
115 lines
4.5 KiB
Markdown
# 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 cluster
|
|
- `Command` — 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 (`Command` on Postgres,
|
|
Mailhog in Production, `CalloutRef` where `IssuerRef` is required).
|
|
Runtime is not a boolean on a god object.
|
|
- **Crichton:** file = mental object; you can reason about `frontend.rs`
|
|
without the mesh. Notation matches: `impl Command for Frontend` *is*
|
|
"we run it with a command".
|
|
- **Parse, don't validate:** no `.image` + `.command` on one blob
|
|
checked later. Missing `impl Command` is 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 = `Frontend` stays
|
|
`Frontend`, refs stay `UrlRef`.
|
|
|
|
**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 `Container` impl.
|
|
- The `Application` builder was deleted. Apps implement `HarmonyApp` and
|
|
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`.
|