Constraint Programming
Constraint programming is a paradigm where you declare what conditions must hold rather than writing step-by-step logic to check them. The engine finds solutions that satisfy all constraints simultaneously.
The key idea
In traditional programming, you filter results after computing them:
// Traditional: compute first, filter afterconst employees = await getAllEmployees();const highEarners = employees.filter(e => e.salary > 100000 && e.age < 40);In constraint programming, you declare the constraints upfront, and the engine finds only matching solutions:
// Constraint programming: declare constraints, engine finds solutionsconst result = await client.inference.backwardChain({ goal: TermInput.byName('employee', { name: FeatureInput.variable('?Name'), salary: FeatureInput.constrainedVar('?Salary', guard('gt', 100000)), age: FeatureInput.constrainedVar('?Age', guard('lt', 40)), }),});The difference seems small in this example, but it becomes dramatic when:
- The search space is large (the engine prunes early, avoiding wasted computation)
- Constraints interact across multiple rules (the engine propagates constraints through the rule chain)
- You need temporal relationships between events (impossible to express as simple filters)
Types of constraints in the Reasoning Layer
Guard constraints
Guards are the simplest constraints — they restrict a variable to values satisfying a comparison:
import { guard, FeatureInput } from '@kortexya/reasoninglayer';
// Numeric guardsFeatureInput.constrainedVar('?Salary', guard('gt', 100000)); // salary > 100000FeatureInput.constrainedVar('?Age', guard('lte', 65)); // age <= 65FeatureInput.constrainedVar('?Score', guard('gte', 0.5)); // score >= 0.5
// String guardsFeatureInput.constrainedVar('?Status', guard('eq', 'active')); // status == "active"FeatureInput.constrainedVar('?Role', guard('ne', 'intern')); // role != "intern"Available operators:
| Operator | Meaning |
|---|---|
lt | Less than |
lte | Less than or equal |
gt | Greater than |
gte | Greater than or equal |
eq | Equal |
ne | Not equal |
Allen temporal constraints
Allen’s interval algebra provides 13 relations between time intervals. This lets you reason about temporal relationships between events — something that’s very difficult with simple boolean logic.
import { allen } from '@kortexya/reasoninglayer';
// "Event A must happen before Event B"allen('before', 'event_a_time', 'event-b-term-uuid')
// "Meeting overlaps with work hours"allen('overlaps', 'meeting_time', 'work-hours-term-uuid')
// "Maintenance window contains the deployment"allen('contains', 'maintenance_window', 'deployment-term-uuid')The 13 Allen relations cover every possible way two time intervals can relate:
before: AAA......BBB (A ends before B starts)meets: AAABBB (A ends exactly when B starts)overlaps: AAAA (A starts before B, ends during B) BBBBstarts: AAAA (same start, A ends first) BBBBBBBduring: AAA (A entirely within B) BBBBBBBfinishes: AAAA (same end, A starts later) BBBBBBBequals: AAAA (same start and end) BBBBEach relation has an inverse (e.g., before/after, overlaps/overlapped_by).
Equality and disequality constraints
Force two variables to be the same value (or different values):
// "X and Y must be the same"{ type: 'Equality', var1: '?X', var2: '?Y' }
// "A and B must be different"{ type: 'Disequality', var1: '?A', var2: '?B' }Bound constraints on sorts
Sorts can declare bound constraints that enforce ordering relationships between features. These are checked whenever a term of that sort is created or updated.
SortBuilder.create('employment_contract') .boundConstraint({ constraint_type: 'upper', // start_date ≤ end_date target: 'end_date', source_path: 'start_date', on_violation: 'fail', // reject the term if violated }) .build();Violation strategies:
| Strategy | Behavior |
|---|---|
fail | Reject the operation (constraint is mandatory) |
warn | Accept but log a warning |
update | Automatically adjust the value to satisfy the constraint |
residuate | Suspend — check again when more information is available |
Constraints in inference
Constraints are most powerful when used in inference rules and goals.
Constraining rule bodies
// "A qualified_candidate is a person with experience >= 5 years// AND salary expectation <= budget"await client.inference.addRule({ term: TermInput.byName('qualified_candidate', { name: FeatureInput.variable('?Name'), }), antecedents: [ TermInput.byName('candidate', { name: FeatureInput.variable('?Name'), experience_years: FeatureInput.constrainedVar('?Exp', guard('gte', 5)), salary_expectation: FeatureInput.constrainedVar('?Ask', guard('lte', 150000)), }), ],});Constraining goals with temporal relations
// "Find tasks that must happen before the deadline// and after lunch"const result = await client.inference.backwardChain({ goal: TermInput.byName('task', { name: FeatureInput.variable('?Task'), time: FeatureInput.variable('?Time'), }), constraints: [ allen('before', '?Time', deadlineTermId), allen('after', '?Time', lunchTermId), ],});Constraint propagation through rule chains
The power of constraints comes from propagation. When multiple rules chain together, constraints from one rule flow into the next:
Rule 1: eligible(Person) :- age(Person, Age >= 18), resident(Person)Rule 2: voter(Person) :- eligible(Person), registered(Person)Rule 3: primary_voter(Person) :- voter(Person), party_member(Person)A query for primary_voter propagates the age >= 18 constraint all the way from Rule 1 through Rules 2 and 3. The engine doesn’t need to check every person and filter — it prunes the search space at each step.
Key takeaways
- Declare, don’t filter — state what must be true, let the engine find solutions
- Guards handle comparisons —
gt,lt,eq,ne,gte,lteon any value type - Allen relations handle time — 13 ways to express temporal relationships between intervals
- Bound constraints enforce structure — sorts can declare ordering relationships between their features
- Constraints propagate — they flow through rule chains, pruning the search space efficiently