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.

The key idea

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: 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 guards
FeatureInput.constrainedVar('?Salary', guard('gt', 100000)); // salary > 100000
FeatureInput.constrainedVar('?Age', guard('lte', 65)); // age <= 65
FeatureInput.constrainedVar('?Score', guard('gte', 0.5)); // score >= 0.5
// String guards
FeatureInput.constrainedVar('?Status', guard('eq', 'active')); // status == "active"
FeatureInput.constrainedVar('?Role', guard('ne', 'intern')); // role != "intern"

Available operators:

OperatorMeaning
ltLess than
lteLess than or equal
gtGreater than
gteGreater than or equal
eqEqual
neNot 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)
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).

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:

StrategyBehavior
failReject the operation (constraint is mandatory)
warnAccept but log a warning
updateAutomatically adjust the value to satisfy the constraint
residuateSuspend — 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

  1. Declare, don’t filter — state what must be true, let the engine find solutions
  2. Guards handle comparisonsgt, 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