composition records implementation members that belong to one interpreted
root instance in the same stage. It adds structural architecture meaning
independently of Concept classification. Members remain separate instances.
For the mental model, see Understand composition.
dialect "example" { version = "0.1.0"
provider "hashicorp/example" { version = ">= 1.0.0" }}
concept "load-balancer" { description = "A load-balancing service."}
rule "application-load-balancer" { match { type = "example_forwarding_rule" }
as = concept.load-balancer
composition { member "proxy" { via = source.proxy_id
match { type = "example_https_proxy" } }
member "backend" { via = member.proxy.backend_id
match { type = "example_backend_service" } } }}Each matching example_forwarding_rule instance is a composition root. Its
proxy_id names the first member; backend_id on a resolved proxy names the
second. The member's via value can match a candidate's declared identity. A
verified saved plan can establish a direct reference to that candidate's
endpoint even when the value is unknown. An unresolved proxy leaves the
backend unresolved too.
Empty composition is invalid. RF has no optional member, alternatives, nested composition, or cardinality operator.
Member order is semantic. A member.* traversal can reference only a
previously declared member. Self-reference and forward reference are invalid.
A member's match uses the same parameters as a Rule's match:
member "backend" { via = member.proxy.backend_id
match { type = "example_backend_service" where = source.enabled == true }}Composition members select the two instance populations present in supported
plan and state exports: resource and data. Rootform language accepts a
wider, 15-value match.kind set, but the other values have no instances in
those exports. See match kinds for the complete
set.
Within a member's match.where, source means the candidate member instance,
not the composition root.
For every root instance, Rootform processes members in authored order:
- Read
viafrom the root or an established earlier member. - Find an eligible instance of the member's kind and type, applying
where. - Establish identity through a known value matching a candidate Rule's declared identity, or through a verified saved-plan traversal to an endpoint.
- Record the established member with its value or traversal evidence; otherwise record an unresolved member with its reason.
A verified traversal to id can establish an uninterpreted member; a
traversal to another attribute cannot. An unresolved member does not stop an
independent later member from resolving.
Composition does not apply the root Rule or Concept to members, invent Relations between them, or remove their Representations. See Fact emissions when a visible relationship is needed.
Composition is evaluated independently for each root instance and stage.
Unresolved members stay listed on that root, alongside any established
members. A later member whose via uses an unresolved earlier member is itself
unresolved with reason unavailable; an independent later member can still
resolve. The root's Rule classification and emissions remain available.
Member resolution is visible through unresolved-member reasons in the Rootform document. Authored source errors still produce compiler diagnostics.
dialect "example" { version = "0.1.0"
provider "hashicorp/example" { version = ">= 1.0.0" }}
rule "empty" { match { type = "example_root" }
composition {}}Empty block produces COMPOSITION_INVALID.
This forward reference is also invalid:
composition { member "backend" { via = member.proxy.backend_id
match { type = "example_backend" } }
member "proxy" { via = source.proxy_id
match { type = "example_proxy" } }}Declare proxy first so member.proxy is available.