Deployment models (.deploy)
Overview
Section titled “Overview”A deployment-projection layer over the other Phero Lang artifacts. The .mox
domain model defines the data, .ddd designs the application services, and
.ifml the interaction flows; the .deploy model declares the logical
architecture (an application of named components and their
connects), reusable profiles (a target plus defaults, policies, and
resource mappings), and deployments (one application projected through
one profile with per-application configure overrides) — with nothing
about containers, manifests, or clouds written down. Parsed by
phero-syntax (parse_deploy, the chumsky parser) at compile time and
lowered and validated by phero-driver (compile_deploy_str) into the
versioned phero_ir::deploy::DeployModel artifact (the phero-ir crate).
The separation the surface enforces is the issue-#49 discipline:
- Architecture — what components exist and how they relate
(
application). Nothing here says where anything runs. - Profile — the constraints and runtime topology of a target
(
profile:target,defaults,policies,mappings). Profiles are reusable across applications. - Deployment — the concrete projection (
deployment ... for ... use ... configure ...). The effective configuration is component baselines ⊕ profile defaults ⊕ overrides (later levels win).
Targets are implementation-provided, never authored: a profile names
one of the closed targets (standalone, dockerCompose, kubernetes,
cloudflareWorkers), and the driver’s capability, engine, and setting
tables define what each can host. The same logical application deploys to
every target it is compatible with — incompatibility is a compile
error, not a generated artifact (“validate before generating”).
The .deploy DSL composes the other surfaces: import "<path>.ddd" /
import "<path>.ifml" pull in pre-compiled design and flow artifacts, and
component bindings (design "<path>#<Module>" / flow "<path>#<Module>") tie components to them by the import path as
written. No in-tree backend exists yet: compose.yaml / Kubernetes
manifest / Worker-bundle generation are the planned consumers of this
artifact (the deployment-plan resource graph is the planned intermediate);
the artifact deliberately carries authored intent only in slice 1.
The normative contract — entry points, resolution semantics, and the
validation rules — is documented on phero_driver::compile_deploy_str; this
page is the language reference.
A worked example
Section titled “A worked example”The fence below is the canonical fixture’s shape
(tests/conformance/deploy/svc.deploy), its imports repointed so it
compiles from this document (the docs fences are compiled by CI):
import "../tests/conformance/ddd/library.ddd"import "../tests/conformance/ifml/app.ifml"
application Svc { component api: api { scaling: autoscaled port: 18082 health: "/healthz" env { LOG_LEVEL = "info" } } component lending: api { design "../tests/conformance/ddd/library.ddd#media" } component web: frontend { flow "../tests/conformance/ifml/app.ifml#Pagination" } component search: worker { requires longRunningProcess } component db: database { engine: postgres storage: managed } connects web -> api connects lending -> db}
profile production_k8s { target: kubernetes defaults { api.runtime: container api.replicas: 3 } mappings { api -> kubernetes.deployment database -> external.postgres } require (api.replicas >= 3) require (api.port == 18082) prohibit (db.storage == "local")}
deployment production for Svc { use production_k8s configure { api.replicas: 4 }}Artifact wire format
Section titled “Artifact wire format”DeployModel is a standalone, versioned wire artifact following the
phero-ir wire format contract (see also the phero_ir::deploy
module docs):
- Versioned independently of the domain-model marker via
DEPLOY_MODEL_FORMAT_VERSION(= 1), theActorModelprecedent.DeployModel::from_jsonrejects absent/wrong versions withIrError::UnsupportedFormatVersion(an absent version reports as 0). - All struct field names serialize as
camelCase(formatVersion). Unit-only enums serialize as bare camelCase strings: component kinds ("api" | "worker" | "database" | "queue" | "objectStore" | "frontend") and targets ("standalone" | "dockerCompose" | "kubernetes" | "cloudflareWorkers"). - Setting values are adjacently tagged:
{"type": "int" | "text" | "word", "value": <primitive>}— the wire-contract rule-2 shape. - Component
enventries serialize as{"name": ..., "value": ...}pairs in declaration order, values verbatim; the key is omitted when a component declares none. - Closed semantic core, extensible platform surface: kinds and targets
are the closed enums; capability names, setting paths, policy
expressions, and mapping resources (
kubernetes.deployment,cloudflare.d1,docker.volume,external.postgres) are plain strings, so new platform resources need no schema change. - Policy expressions are verbatim source text between the policy’s
parentheses — the IR never parses or reformats them (the
when-condition rule). - Resolution is validation-only: the artifact keeps every name, setting, and binding as authored (component baselines stay single-segment). Effective settings are computed for validation and never serialized back.
- Unknown fields are ignored on deserialize; every field added after v1
must keep the
#[serde(default)]+ skip discipline.
Minimal artifact: an empty model serializes as exactly
{"formatVersion":2}.
let designs: Vec<(String, phero_ir::ddd::DddModel)> = vec![];let flows: Vec<(String, phero_ir::ifml::IfmlModel)> = vec![];let compilation = phero_driver::compile_deploy_str("svc.deploy", source, &designs, &flows);let json = compilation.model.unwrap().to_json_pretty()?; // versioned artifactlet model = phero_ir::deploy::DeployModel::from_json(&json)?; // version-gated reloadGrammar
Section titled “Grammar”The normative EBNF for the .deploy surface lives exactly once, in the
parser module docs under .deploy deployment
sources
(crates/phero-syntax/src/parser/mod.rs) — the single authority for
deploy_file, application_decl, component_decl, profile_decl,
policy_decl, deployment_decl, and the setting/mapping productions;
this page keeps only the language-level rules. The .deploy keyword
tables in crates/phero-syntax/src/deploy.rs are the keyword authority the
parser and the formatter share.
Language-level rules:
application <name> { component <name>: <kind> { ... } ... connects a -> b ... }— the logical architecture. Kinds are the closed vocabularyapi,worker,database,queue,objectStore,frontend. A component body carriesname: valuebaseline settings,requires <capability>, ...clauses,env { NAME = "value" ... }blocks, anddesign/flowbindings.profile <name> { target: <target> ... }— a reusable configuration.defaults { component.setting: value }apply to every deployment selecting the profile;mappings { kind -> platform.resource }say how a kind translates into platform resources;require (...)/prohibit (...)policies constrain every such deployment.deployment <name> for <application> { use <profile> configure { ... } }— one projection.configureoverrides beat profile defaults, which beat component baselines.- Policies are phero-expr conditions over the effective settings:
api.replicas >= 3,db.storage == "local",&&/||/!, integer arithmetic. They are parsed and type-checked against the deployed application’s components (an unknown component or setting is a type error) and evaluated against each deployment’s effective settings — a violatedrequireor satisfiedprohibit(including through overrides) is a compile error. The expression rules R1 (checked arithmetic) and R4 (no zero divisor) apply. An unset setting referenced by a policy is an evaluation error: give the component a baseline or the profile a default. Env entries are not features of that universe —api.env.FOOis a type error, by design. design/flowbindings are"<path>"or"<path>#<Module>"— the path must match one of the file’s imports as written and the module must exist in the imported design/flow.- Every
.deployword is a contextual identifier (the.ddddiscipline): special only in its grammar position, an ordinary name everywhere else, escapable with^. The kind and target vocabularies are deliberately not syntax — the driver validates them, so a word likecontainer(a.moxkeyword) works as a setting value. The same tolerance applies toenventry names: a bare word, keyword tokens included (when = "always"is an env entry). - Imports come first:
import "<path>"(no trailing;, the.actorproduction) must precede the first declaration; a laterimportis a reported, recovered syntax error. Only.dddand.ifmlimports are supported. - The three declaration kinds may be interleaved in any order; the artifact groups them per kind and the formatter canonicalizes to import → application → profile → deployment order (source order kept within a kind).
- The
->arrow ofconnectsandmappingsis a real lexer token of the shared chumsky lexer; it never appears in a parsed grammar position of the other surfaces (raw bodies are span-sliced verbatim).
Operational vocabulary (issue #135)
Section titled “Operational vocabulary (issue #135)”The per-kind setting tables carry the operational settings a deployment projection needs, typed and closed like every other setting:
port(integer) — the listener port, on every kind.health(text) — the health-check path, onapiandworker.cors(text) — the comma-separated allowed-origin list, on the browser-facing kindsapiandfrontend(v1 keeps the list opaque; per-origin validation is a future concern).
They participate in everything settings do: defaults, overrides,
effective-settings resolution, and policies for free (require (api.port == 18082) type-checks because the policy universe is built
from the same tables — port types as int, health/cors as
string). Values beyond the type are platform concerns and carried as
authored.
Env is the opaque twin. An env { NAME = "value" ... } block
declares string environment variables verbatim, on any component. Env
entries are deliberately not settings: they never join the
effective-settings map, are never policy-addressable, and are validated
only for empty and duplicated names per component. Secrets and platform
knobs (LOG_LEVEL, DATABASE_URL) belong here — author intent carried
to the backend, semantics kept in the closed setting tables. Env values
are quoted strings only.
Resolution & validation
Section titled “Resolution & validation”A deployment file is self-contained for names (applications, profiles,
components, deployments resolve within the file) and composition-open for
content (design/flow artifacts arrive pre-compiled — the CLI gathers and
compiles them exactly like pherolang check on those surfaces). The driver
stays filesystem-free.
Validation rules (any error-severity diagnostic drops the artifact; warnings do not):
-
Syntax — parse errors are reported against the
.deployfile. -
Imports — every import must name a provided
.ddddesign or.ifmlflow, elseimported file "<path>" was not provided; other extensions areunsupported import. -
Kinds and capabilities — component kinds and
requiresnames must be in the closed vocabularies (typos are errors, not vacuous requirements). -
Settings — component baselines are single-segment
name: valuelines from the kind’s vocabulary; defaults and overrides are two-segmentcomponent.setting: valuelines naming a component of the deployed application; word values must be in their list; integer and text settings take the matching literal kind; a path is declared once per level. Anenv { NAME = "value" ... }block is not settings: entries are opaque strings lowered verbatim, checked only for empty (env name must not be empty) and duplicated (duplicate env 'NAME' on component 'C') names; values pass through untouched and never join the effective settings. -
Bindings — a binding’s path must match an import, and a
#<Module>suffix must name a module of the imported design/flow. -
Targets — a profile must carry a
target:line naming a known target. -
References —
connectsendpoints name components of their application;deployment ... fornames an application andusenames a profile of this file. -
Capabilities — per component: intrinsic requirements (a
workerneedslongRunningProcess, adatabasesqlDatabase), conditional requirements (engine: sqliteaddspersistentFilesystem;runtime: containeraddscontainer), and declaredrequiresmust all be provided by the target. Target capability tables:standalone— longRunningProcess, persistentFilesystem, sqlDatabase, backgroundWorker, statefulProcess;dockerCompose,kubernetes— those pluscontainerandhorizontalScaling;cloudflareWorkers— sqlDatabase, horizontalScaling, edgeExecution (no long-running processes, no local filesystem).
The closed vocabulary is also the semantic graph’s
platformCapabilitynode kind: one node per application and word, with no declaration site (see the graph’s platform capabilities). -
Engines — a database’s effective engine must be one the target hosts (
standalone: sqlite;dockerCompose: sqlite, postgres;kubernetes: postgres;cloudflareWorkers: d1). A postgres database is never silently mapped onto D1. -
Policies — see above: parsed, typed boolean, evaluated against effective settings; violations are errors naming the deployment and the policy. The typed operational settings are addressable (
port,health,cors); env entries are not.
Diagnostics are ordered deterministically: parse diagnostics first, then the file’s semantic diagnostics in declaration order (applications, profiles, deployments — each in source order).
Components are also the anchor the .evt subscription layer resolves
against: in a checked application (pherolang check <dir>, the union
integrity pass phero_driver::check_integrity, register row V12) a
subscription consumer must name a declared component — any application’s
— else the event contract’s delivery is a dead inbox and the batch
errors. The deploy file itself is never changed by that pass; checking a
deployment alone stays per-surface and silent.
Tooling
Section titled “Tooling”pherolang check <file>.deploy— compile and render diagnostics (including the imported designs/flows, which are gathered and compiled exactly likepherolang checkon them; a non-compiling import is a clean error).pherolang ir <file>.deploy [-o out]— emit the versionedDeployModelJSON.pherolang fmt <file>.deploy— the token-based formatter (canonical section order, one member per line, verbatim policy conditions).pherolang artifact check svc.deploy.json— wire-format validation; the artifact classifies as theDeployModelkind.- The LSP does not publish
.deploydiagnostics yet (the.evtprecedent): the surface compiles through the same one-shot entry points, so editor wiring is a follow-up, not a contract change.
The examples/ side ships support.deploy — one logical application
bound to support.ddd#desk and support.ifml, projected through a
kubernetes profile (postgres, mappings, policies) and a cloudflareWorkers
profile (D1) — byte-pinned against examples/artifacts/ by
crates/phero-driver/tests/examples_artifacts.rs.