Skip to content

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.

In traditional programming, you filter results after computing them:

// Traditional: compute first, filter after
const 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 solutions
const result = await client.inference.backwardChain({
goal: psi('employee', {
name: '?Name',
salary: constrained('?Salary', guard('gt', 100000)),
age: constrained('?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

Section titled “Types of constraints in the Reasoning Layer”

Guards are the simplest constraints — they restrict a variable to values satisfying a comparison:

import { guard, constrained, psi } from '@kortexya/reasoninglayer';
// Numeric guards
constrained('?Salary', guard('gt', 100000)); // salary > 100000
constrained('?Age', guard('lte', 65)); // age <= 65
constrained('?Score', guard('gte', 0.5)); // score >= 0.5
// String guards
constrained('?Status', guard('eq', 'active')); // status == "active"
constrained('?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’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, constrained, guard, psi } 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)
BBBB
starts: AAAA (same start, A ends first)
BBBBBBB
during: AAA (A entirely within B)
BBBBBBB
finishes: AAAA (same end, A starts later)
BBBBBBB
equals: AAAA (same start and end)
BBBB

Each relation has an inverse (e.g., before/after, overlaps/overlapped_by).

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' }

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({
constraintType: 'upper', // start_date ≤ end_date
target: 'end_date',
sourcePath: 'start_date',
onViolation: '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 are most powerful when used in inference rules and goals.

// "A qualified_candidate is a person with experience >= 5 years
// AND salary expectation <= budget"
await client.inference.addRule({
term: psi('qualified_candidate', {
name: '?Name',
}),
antecedents: [
psi('candidate', {
name: '?Name',
experience_years: constrained('?Exp', guard('gte', 5)),
salary_expectation: constrained('?Ask', guard('lte', 150000)),
}),
],
});

Constraining goals with temporal relations

Section titled “Constraining goals with temporal relations”
// "Find tasks that must happen before the deadline
// and after lunch"
const result = await client.inference.backwardChain({
goal: psi('task', {
name: '?Task',
time: '?Time',
}),
constraints: [allen('before', '?Time', deadlineTermId), allen('after', '?Time', lunchTermId)],
});

Constraint propagation through rule chains

Section titled “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.

  1. Declare, don’t filter — state what must be true, let the engine find solutions
  2. Guards handle comparisons — gt, lt, eq, ne, gte, lte on any value type
  3. Allen relations handle time — 13 ways to express temporal relationships between intervals
  4. Bound constraints enforce structure — sorts can declare ordering relationships between their features
  5. Constraints propagate — they flow through rule chains, pruning the search space efficiently