Files
harmony/docs/adr/029-application-components.md
Jean-Gabriel Gill-Couture eea006535b refactor: drop harmony_app landfill
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.
2026-09-08 22:10:34 -04:00

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`.