Skip to content

A Fact is a named, typed, sourced value that forms the ground layer of the evaluation model. Facts are assembled into an immutable FactSet before rule evaluation begins. No fact is derived by any rule, produced by any operation, or computed by any internal evaluation. Facts are the provenance root -- every provenance chain in the contract terminates at one or more facts.

DSL Syntax

hcl
fact <fact_id> {
  type:    <BaseType>
  source:  <freetext_string> | <source_id> { path: "<path>" }
  default: <value>                                              // optional
}

Fields

FieldRequiredDescription
typeYesOne of the twelve base types. Determines what values are acceptable.
sourceYesNames the external system providing this value. Either a freetext string or a structured reference to a declared Source.
defaultNoFallback value when the runtime does not supply the fact. Must type-check against the declared type.

All Twelve Base Types with Examples

hcl
// Bool -- true or false
fact delivery_confirmed {
  type:    Bool
  source:  "tracking_service.delivered"
  default: false
}

// Int -- bounded integer with min/max
fact cargo_weight_kg {
  type:   Int(min: 0, max: 1000000)
  source: "cargo_service.total_weight_kg"
}

// Decimal -- fixed-point with precision and scale
fact tax_rate {
  type:   Decimal(precision: 10, scale: 4)
  source: "tax_service.effective_rate"
}

// Text -- bounded-length UTF-8 string
fact customer_name {
  type:   Text(max_length: 200)
  source: "crm.customer_name"
}

// Enum -- finite set of named values
fact risk_level {
  type:   Enum(values: [low, medium, high, critical])
  source: "risk_engine.assessment"
}

// Date -- RFC 3339 full-date (YYYY-MM-DD)
fact order_date {
  type:   Date
  source: "order_service.created_date"
}

// DateTime -- RFC 3339 date-time, UTC-normalized
fact submission_time {
  type:   DateTime
  source: "portal.submitted_at"
}

// Money -- currency-tagged decimal amount
fact escrow_amount {
  type:   Money(currency: "USD")
  source: "escrow_service.balance"
}

// Duration -- bounded time span
fact processing_deadline {
  type:   Duration(unit: "days", min: 1, max: 30)
  source: "sla_service.processing_window"
}

// Record -- named product type with typed fields
fact shipping_address {
  type: Record(fields: {
    street: Text(max_length: 256),
    city:   Text(max_length: 128),
    state:  Text(max_length: 64),
    zip:    Text(max_length: 10)
  })
  source: "order_service.shipping"
}

// List -- bounded homogeneous list
fact line_items {
  type:   List(element_type: LineItemRecord, max: 100)
  source: "order_service.line_items"
}

// TaggedUnion -- sum type with named variants
fact payment_method {
  type: TaggedUnion(variants: {
    CreditCard:   Record(fields: { last_four: Text(max_length: 4), brand: Text(max_length: 20) }),
    BankTransfer: Record(fields: { routing: Text(max_length: 9), account: Text(max_length: 4) }),
    Wire:         Record(fields: { swift_code: Text(max_length: 11) })
  })
  source: "billing_service.payment_method"
}

Operators by Type

Each base type supports a defined set of operators in predicate expressions. See Symbols & Operators for the complete reference with Unicode and ASCII forms.

TypeOperators
Bool= != and or not
Int, Decimal= != < <= > >= + - * literal
Money= != < <= > >= + - (same currency only)
Text= != (exact equality only -- no pattern matching)
Enum= !=
Date, DateTime= != < <= > >=
Duration= != < <= > >= + -
Record= != (field-wise), field access via .
TaggedUnion= != (tag + payload), tag-embedded access via .tag.field
Listlen(list), element access list[i], bounded quantification

Text comparison is exact only. Pattern matching (regex, substring, prefix, glob) is not supported. Pattern-based classification must be pre-computed into a Bool or Enum fact by the executor.

Freetext vs. Structured Source References

Facts support two source forms:

Freetext source (simple string)

hcl
fact buyer_requested_refund {
  type:   Bool
  source: "buyer_portal.refund_requested"
}

The freetext source is a dot-separated string naming the external system and data point. The elaborator validates only that it is non-empty.

Structured source (references a declared Source)

hcl
source order_service {
  protocol:    http
  base_url:    "https://api.orders.com/v2"
  description: "Order management REST API"
}

fact escrow_amount {
  type:   Money(currency: "USD")
  source: order_service { path: "orders/{id}.balance" }
}

A structured source references a declared Source construct by id, with a path field for the specific data point. The elaborator validates that the source_id references an existing Source declaration (constraint C-SRC-06).

Default Values

hcl
fact dispute_filed {
  type:    Bool
  source:  "dispute_service.status"
  default: false
}

fact compliance_threshold {
  type:    Money(currency: "USD")
  source:  "compliance_db.threshold"
  default: 10000.00
}

When a default is declared:

  • If the runtime provides a value, it is type-checked and used.
  • If the runtime does not provide a value, the default is used with assertion_source: "contract" in provenance.
  • If no default is declared and the value is missing, evaluation aborts with "missing fact: <fact_id>".

FactSet Assembly

Assembly follows this sequence for each declared fact:

  1. If the runtime provides a value for the fact: type-check it against the declared type. If type-check fails, abort. Otherwise, add to FactSet with assertion_source: "external".
  2. If no value is provided but a default exists: add the default to FactSet with assertion_source: "contract".
  3. If no value and no default: abort with "missing fact: <fact_id>".

For List-typed facts, assembly additionally verifies that the list length does not exceed the declared max and that every element type-checks against the declared element_type.

The assembled FactSet is immutable from this point forward. No rule, operation, or flow modifies the FactSet.

No Aggregation

A common incorrect assumption is that aggregate functions (sum, count, average, min, max) can be computed over List-typed facts within rule bodies or predicate expressions. This is not permitted. Aggregates are derived values -- they must arrive as facts from external systems.

hcl
// WRONG -- Tenor has no aggregate functions
rule requisition_total {
  stratum: 0
  when:    true
  produce: verdict total(sum(item.amount for item in line_items))
}
hcl
// CORRECT -- aggregate arrives as a pre-computed fact
fact requisition_total {
  type:   Money(currency: "USD")
  source: "procurement_service.requisition_total"
}

This is a deliberate design constraint. Aggregates depend on the completeness and correctness of the underlying data. A contract cannot verify it received all items. Pretending the contract can compute a trustworthy aggregate is dishonest about the trust boundary.

Numeric Model

All numeric computation in Tenor uses fixed-point decimal arithmetic with these properties:

PropertyValue
Maximum significant digits28
Scale range0--28 fractional digits
Rounding modeRound-half-to-even (IEEE 754)
Overflow behaviorTyped abort (never silent wraparound)
InfinityNot representable
NaNNot representable
Signed zeroNot representable

Type promotion rules ensure cross-type arithmetic is well-defined:

  • Int(a,b) + Int(c,d) produces Int(a+c, b+d)
  • Int(a,b) * literal_n produces Int(a*n, b*n) if n >= 0
  • Decimal(p1,s1) + Decimal(p2,s2) produces Decimal(max(p1,p2)+1, max(s1,s2))
  • Int mixed with Decimal: the Int is promoted to Decimal(ceil(log10(max(|min|,|max|)))+1, 0), then Decimal rules apply
  • Integer literals are typed as Int(n, n). Decimal literals are typed as Decimal(total_digits, fractional_digits).

Duration promotion: Cross-unit Duration arithmetic promotes to the smaller unit. For example, Duration(days) + Duration(hours) produces Duration(hours) with days converted at 24 hours per day.

Full Working Example

A complete escrow contract showing facts in context:

hcl
// --- Sources ---
source order_service {
  protocol:    http
  base_url:    "https://api.orders.com/v2"
  description: "Order management REST API"
}

source compliance_db {
  protocol:    database
  dialect:     postgres
  description: "Compliance reporting database"
}

// --- Facts ---
fact escrow_amount {
  type:   Money(currency: "USD")
  source: order_service { path: "orders/{id}.balance" }
}

fact compliance_threshold {
  type:    Money(currency: "USD")
  source:  compliance_db { path: "compliance_thresholds.amount" }
  default: 10000.00
}

fact delivery_confirmed {
  type:    Bool
  source:  "tracking_service.delivered"
  default: false
}

fact buyer_requested_refund {
  type:   Bool
  source: "buyer_portal.refund_requested"
}

// --- Personas ---
persona buyer
persona seller
persona escrow_agent
persona compliance_officer

// --- Entity ---
entity EscrowAccount {
  states:  [held, released, refunded, disputed]
  initial: held
  transitions: [
    (held, released),
    (held, refunded),
    (held, disputed),
    (disputed, released),
    (disputed, refunded)
  ]
}

// --- Rules ---
rule within_threshold {
  stratum: 0
  when:    escrow_amount <= compliance_threshold
  produce: verdict within_threshold { payload: Bool = true }
}

rule delivery_ok {
  stratum: 0
  when:    delivery_confirmed = true
  produce: verdict delivery_ok { payload: Bool = true }
}

rule can_auto_release {
  stratum: 1
  when:    verdict_present(within_threshold) and verdict_present(delivery_ok)
  produce: verdict can_auto_release { payload: Bool = true }
}

// --- Operations ---
operation release_escrow {
  personas: [escrow_agent]
  require:  verdict_present(can_auto_release)
  effects:  [EscrowAccount: held -> released]
  outcomes: [released]
}

operation refund_buyer {
  personas: [escrow_agent]
  require:  buyer_requested_refund = true
  effects:  [EscrowAccount: held -> refunded]
  outcomes: [refunded]
}

Constraints

  • Fact identifiers are unique within a contract.
  • The complete set of fact identifiers is statically enumerable from the contract.
  • Fact identifiers are fixed at contract definition time. No fact may be dynamically named or dynamically typed.
  • C-SRC-06 -- If a fact uses a structured source, the source_id must reference a declared Source construct. Unresolved references are elaboration errors.

Common Mistakes

Computing values inside the contract. Facts come from external systems. If you need a derived value (like "total weight of all items"), that value must be computed externally and supplied as a fact.

Missing source field. Every fact must declare a source. Even manual input should be "manual.user_input" or reference a manual protocol Source.

Overly broad types. Use the most specific type. Text(max_length: 255) for a country code is less useful than Enum(values: [US, CA, GB, DE]).

Forgetting defaults for optional data. If a fact might not be available during some evaluations, provide a default. Otherwise evaluation aborts.

Assuming pattern matching on Text. You cannot write name matches "LEGAL-.*". Text supports only exact = and !=. Pre-classify values into Bool or Enum facts.

Aggregation in predicates. There is no sum(), count(), or average() anywhere in the language. Aggregates must arrive as pre-computed facts.

How Facts Connect to Other Constructs

  • Rules read facts via fact_ref in their when predicates (stratum 0 rules reference only facts).
  • Operations read facts via fact_ref in their precondition predicates.
  • Flows use the frozen FactSet (snapshotted at flow initiation) for BranchStep conditions.
  • Sources provide the infrastructure metadata for structured source references on facts.
  • Provenance chains terminate at facts -- every verdict, operation outcome, and flow outcome traces back to the facts that produced it.

Static Analysis Properties

  • S5 (Verdict and outcome space) depends on the fact types that feed stratum 0 rules.
  • S7 (Evaluation complexity bounds) incorporates the max bounds on List-typed facts in quantified expressions.
  • The complete set of fact identifiers is derivable from the contract without execution.