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
fact <fact_id> {
type: <BaseType>
source: <freetext_string> | <source_id> { path: "<path>" }
default: <value> // optional
}Fields
| Field | Required | Description |
|---|---|---|
type | Yes | One of the twelve base types. Determines what values are acceptable. |
source | Yes | Names the external system providing this value. Either a freetext string or a structured reference to a declared Source. |
default | No | Fallback value when the runtime does not supply the fact. Must type-check against the declared type. |
All Twelve Base Types with Examples
// 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.
| Type | Operators |
|---|---|
| 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 |
| List | len(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)
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)
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
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:
- 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". - If no value is provided but a default exists: add the default to FactSet with
assertion_source: "contract". - 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.
// WRONG -- Tenor has no aggregate functions
rule requisition_total {
stratum: 0
when: true
produce: verdict total(sum(item.amount for item in line_items))
}// 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:
| Property | Value |
|---|---|
| Maximum significant digits | 28 |
| Scale range | 0--28 fractional digits |
| Rounding mode | Round-half-to-even (IEEE 754) |
| Overflow behavior | Typed abort (never silent wraparound) |
| Infinity | Not representable |
| NaN | Not representable |
| Signed zero | Not representable |
Type promotion rules ensure cross-type arithmetic is well-defined:
Int(a,b) + Int(c,d)producesInt(a+c, b+d)Int(a,b) * literal_nproducesInt(a*n, b*n)if n >= 0Decimal(p1,s1) + Decimal(p2,s2)producesDecimal(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 asDecimal(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:
// --- 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_idmust 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_refin theirwhenpredicates (stratum 0 rules reference only facts). - Operations read facts via
fact_refin theirpreconditionpredicates. - 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.