An applied Rule may emit directed facts from its instance to a target
endpoint in the same stage. to declares the target's required Rule or
Concept. via reads evidence on the source instance. A Terraform dependency
alone creates no Context, Relation, or Contribution.
For the mental model, see Choose an architectural fact and Evidence and target resolution.
dialect "example" { version = "0.1.0"
provider "hashicorp/aws" { version = ">= 6.0.0, < 7.0.0" }}
rule "vpc" { match { type = "aws_vpc" } as = rf.concept.virtual-network identity { attributes = ["id"] } endpoint { attributes = ["id"] }}
rule "subnet" { match { type = "aws_subnet" } as = rf.concept.subnet
context { as = rf.context.network to = rf.concept.virtual-network via = source.vpc_id on_null = "absent" on_empty = "absent"
match { by = target.id strategy = "exact" } }}A subnet can establish network Context through its known vpc_id, or through a verified saved-plan reference to the VPC's declared endpoint. The closure records how each fact was proved.
Each emission is a block inside a rule. It may appear more than once, has no
nested blocks except one optional, unlabeled match, and uses these shared
parameters:
A missing on_null or on_empty is a compile error. Without verified endpoint reference evidence, unknown values remain indeterminate(unknown_until_apply) and sensitive values remain indeterminate(sensitive) regardless of those declarations. A verified reference can establish an endpoint without comparing its sensitive value. A list fans out element by element; established facts remain even if another element makes the overall closure indeterminate. A nested list, map, or Boolean element is an unsupported endpoint shape (VIA_VALUE_SHAPE).
context { as = rf.context.network to = rf.concept.virtual-network via = source.vpc_id on_null = "absent" on_empty = "absent"}context "placement" { ... } introduces or reuses a local Context instead of referencing one through as. Both syntaxes need the common emission parameters.
Exactly one of a label or as is required. Both or neither produce FACT_INVALID. A Context states its declared placement meaning, not live network reachability.
relation "reads-from" { to = concept.database via = source.database_id on_null = "absent" on_empty = "indeterminate"}An unlabeled relation { as = relation.reads-from ... } references an existing local Relation. Exactly one of label or as is required. Relations describe only the declared predicate; Rootform never turns every dependency into a Relation. The RF Vocabulary has no predefined Relations.
contribution { to = rf.concept.object-storage-container via = source.bucket on_null = "absent" on_empty = "absent"}Contribution has neither label nor as. It can be queried from the target with contributions. Duplicated emissions of the same identity in one Rule fail with DUPLICATE_ID; a fact reached through distinct valid evidence is deduplicated while retaining provenance.
Without a nested match, a verified saved-plan traversal can identify a target through a declared endpoint path. The target must be represented by an applied Rule satisfying to; a source dependency or a matching value alone does not establish a direct reference. Traversal evidence states which expressions qualify.
The target must:
- have a Representation in the selected stage;
- have an applied Rule;
- satisfy the exact Concept or Rule named by
to.
Without match, a verified saved-plan traversal may identify a declared endpoint directly. For value comparison, add one match block:
match { by = [target.id, target.arn] strategy = "exact"}The block belongs inside one Context, Relation, or Contribution emission. It has no label or nested blocks and appears at most once in that emission.
For a local Concept or Rule target, each by path must appear in the identity
of an eligible target Rule. A shared rf Concept target is exempt from this
check when compiling one Dialect because eligible candidates may belong to
other Dialects; whole-set validation checks the paths against target Rule
identities. During resolution, if a candidate's Rule declares none of the
listed paths, Rootform compares the candidate using that Rule's own identity
attributes.
exact compares known values. dot-ancestor accepts a dot-delimited ancestor, choosing the most specific candidate. last-segment compares the final /-separated segment. Rootform tries by paths in order. A candidate with unknown, sensitive, or unavailable identity cannot be discarded to manufacture a unique match. Duplicate known identities produce DUPLICATE_IDENTITY; an uncomparable candidate can leave indeterminate(uncomparable_candidate). A known value and verified traversal that point to different targets produce EVIDENCE_CONFLICT and indeterminate(reference_ambiguous).
--plan-file pairs the saved plan with its plan JSON and reads the configuration snapshot. A bare reference or single interpolation through variables, locals, or module outputs can pair the referenced managed or data instance with a target Rule's endpoint attribute. A tuple written at the emitted attribute, or within one static block, pairs elements separately. This is Planned-stage evidence. Functions, operators, conditionals, dynamic indexes, and transformed references do not establish endpoint identity. A fact records value, traversal, or both as its evidence kind. See Traversals and scope.
via = provider.host follows the emitting instance's bound provider block. Only a verified Planned-stage direct reference or supported pass-through can establish its endpoint. A literal, transformed expression, plan without verified saved plan, state JSON, historical stage, OpenTofu provider for_each, or JSON provider configuration leaves indeterminate(unavailable). Rootform does not read literal provider configuration values.
An interpreted data resource instance is a data endpoint. It needs an applied Rule just like a managed target. A known unmatched value may become an external endpoint only with external = "allow" and no eligible unresolved in-scope candidate. Put a source comment beside that choice explaining why the target may live outside the inventory. External means declared reference, not verification of a remote object.
disclose = "none" keeps external identity in memory. "record" permits it in the Rootform JSON document; "report" also permits it in text, Markdown, HTML, and SARIF. Sensitive values never enter any tier. When several emissions reach one external endpoint, the most restrictive tier wins. Managed and data endpoints are named by instance, never by copied attribute values. Saved plans, plan JSON, and state JSON can contain clear-text secrets: keep them out of Git and public artifacts. Rootform masks sensitive values before architecture output, but names and topology in its outputs still deserve internal handling.
Each source instance and emission has one closure in its stage:
Other reasons include unknown_until_apply, sensitive, ambiguous_unknown, uncomparable_candidate, duplicate_identity, identity_incomplete, reference_ambiguous, unavailable, and external_denied. A missing attribute path on an emitted instance yields EMISSION_PATH_UNDEFINED and indeterminate(unavailable), not absence. A wrong target Concept does not get coerced; an unresolved target does not turn into an external endpoint. See Diagnostics and limits.
EMISSION_PATH_UNDEFINED, VIA_VALUE_SHAPE, DUPLICATE_IDENTITY, and EVIDENCE_CONFLICT warn that a fact was not safely established. They are evidence limits, not instructions to suppress the closure. The evaluation rules explain why missing facts under indeterminate closures cannot make a negative policy assertion pass.
A Context or Relation with both a label and as fails with FACT_INVALID. A missing to or via fails likewise. Missing null or empty policy fails with EMISSION_ON_NULL_REQUIRED or EMISSION_ON_EMPTY_REQUIRED. An undeclared match.by path for a local target fails with MATCH_IDENTITY_UNDECLARED.