Hone logo
Hone
Problems

Advanced Conditional Inference Engine in TypeScript

This challenge involves building a flexible and powerful conditional inference engine in TypeScript. Such an engine is crucial for applications requiring dynamic decision-making based on complex, nested conditions, such as business rule engines, configuration systems, or intelligent agents. You will implement a system that can evaluate a set of rules against given data and determine which actions should be taken.

Problem Description

You need to design and implement a ConditionEngine class in TypeScript that can evaluate a list of Rule objects against a given Context object. Each Rule has a condition and an action. The condition is a structured representation of logical expressions (AND, OR, NOT) involving properties within the Context. The action is a function that is executed if the rule's condition evaluates to true.

The engine should support:

  • Nested conditions: Conditions can be arbitrarily nested using logical operators.
  • Multiple data types: Conditions should be able to compare various data types (numbers, strings, booleans, arrays).
  • Logical operators: Support for AND (&&), OR (||), and NOT (!) operators.
  • Comparison operators: Support for equality (===), inequality (!==), greater than (>), less than (<), greater than or equal to (>=), and less than or equal to (<=).
  • Array operations: Support for checking if an array contains a specific element (includes) or if all elements satisfy a condition.
  • Execution of actions: When a rule's condition is met, its associated action should be executed with the Context as an argument.

The ConditionEngine should have a method evaluate(context: Context): Promise<void> that iterates through all registered rules, evaluates their conditions against the provided context, and executes the actions of all satisfied rules. The evaluation should be asynchronous to allow for potential future extensions involving I/O or complex computations within conditions.

Key Requirements:

  1. Context Type: Define a flexible Context type that can hold any key-value pairs.
  2. Condition Structure: Define a discriminated union type for Condition that represents different types of logical and comparison operations.
    • LogicalCondition: Contains an operator (&&, ||, !) and an array of other Condition objects.
    • ComparisonCondition: Contains a key (path to a property in Context), an operator (e.g., ===, >, includes), and a value to compare against.
  3. Rule Type: Define a Rule type with a condition and an action (a function that takes Context and returns Promise<void>).
  4. ConditionEngine Class:
    • A constructor that can optionally take an initial list of Rule objects.
    • An addRule(rule: Rule): void method.
    • An evaluate(context: Context): Promise<void> method.
  5. Condition Evaluation Logic: Implement a robust recursive function to evaluate the Condition structure against a Context. This function should handle:
    • Accessing nested properties in Context (e.g., user.address.city).
    • Applying logical operators.
    • Applying comparison operators.
    • Handling array includes checks.

Expected Behavior:

  • Rules are evaluated sequentially.
  • If a rule's condition evaluates to true, its action is invoked.
  • All rules whose conditions are met should have their actions executed.
  • The evaluate method should return a Promise that resolves when all applicable actions have completed.

Edge Cases:

  • Empty Context object.
  • Conditions referencing non-existent keys in the Context.
  • Conditions with unsupported operators.
  • Conditions with invalid nesting.
  • Context values being null or undefined.

Examples

Example 1: Simple Comparison and AND Logic

// Input Definitions (for clarity, not actual code for input)
type Context = {
    user: {
        age: number;
        isActive: boolean;
    };
    order: {
        total: number;
        items: string[];
    };
};

type Rule = {
    condition: Condition;
    action: (context: Context) => Promise<void>;
};

// Example Condition structure:
// {
//     operator: '&&',
//     conditions: [
//         { key: 'user.age', operator: '>=', value: 18 },
//         { key: 'user.isActive', operator: '===', value: true }
//     ]
// }

const mockAction1 = jest.fn((context: Context) => console.log("Action 1 executed"));
const mockAction2 = jest.fn((context: Context) => console.log("Action 2 executed"));

const rules: Rule[] = [
    {
        condition: {
            operator: '&&',
            conditions: [
                { key: 'user.age', operator: '>=', value: 18 },
                { key: 'user.isActive', operator: '===', value: true }
            ]
        },
        action: mockAction1
    },
    {
        condition: {
            operator: '||',
            conditions: [
                { key: 'order.total', operator: '>', value: 100 },
                { key: 'order.items', operator: 'includes', value: 'premium_service' }
            ]
        },
        action: mockAction2
    }
];

const context: Context = {
    user: { age: 25, isActive: true },
    order: { total: 150, items: ['standard_item'] }
};

// Expected Output (after calling engine.evaluate(context)):
// Action 1 executed (Rule 1 condition met: 25 >= 18 AND true === true)
// Action 2 executed (Rule 2 condition met: 150 > 100 OR 'standard_item' includes 'premium_service')

// Explanation:
// Rule 1's condition evaluates to true because the user is 25 (>= 18) and isActive is true.
// Rule 2's condition evaluates to true because the order total is 150 (> 100), even though 'premium_service' is not in items.
// Both mockAction1 and mockAction2 should be called.

Example 2: Nested NOT and Array includes

// Using the same Context and Rule types as Example 1

const mockAction3 = jest.fn((context: Context) => console.log("Action 3 executed"));
const mockAction4 = jest.fn((context: Context) => console.log("Action 4 executed"));

const rules: Rule[] = [
    {
        condition: {
            operator: '!',
            conditions: [ // NOT block
                {
                    operator: '&&',
                    conditions: [
                        { key: 'user.age', operator: '<', value: 18 },
                        { key: 'user.isActive', operator: '===', value: false }
                    ]
                }
            ]
        },
        action: mockAction3
    },
    {
        condition: {
            key: 'order.items',
            operator: 'includes',
            value: 'free_trial'
        },
        action: mockAction4
    }
];

const context: Context = {
    user: { age: 16, isActive: true },
    order: { total: 50, items: ['basic_item', 'free_trial'] }
};

// Expected Output (after calling engine.evaluate(context)):
// Action 3 executed (Rule 1 condition met: NOT (16 < 18 AND true === false) -> NOT (true AND false) -> NOT(false) -> true)
// Action 4 executed (Rule 2 condition met: ['basic_item', 'free_trial'] includes 'free_trial')

// Explanation:
// Rule 1's condition: The inner AND condition (16 < 18 AND true === false) is (true AND false), which is false. The NOT operator inverts this to true. So, Action 3 is executed.
// Rule 2's condition: The 'order.items' array includes 'free_trial'. So, Action 4 is executed.

Example 3: Edge Case - Non-existent Key and undefined Comparison

// Using the same Context and Rule types as Example 1

const mockAction5 = jest.fn((context: Context) => console.log("Action 5 executed"));

const rules: Rule[] = [
    {
        condition: {
            operator: '&&',
            conditions: [
                { key: 'user.nonExistentProp', operator: '===', value: undefined },
                { key: 'user.age', operator: '>', value: 0 }
            ]
        },
        action: mockAction5
    }
];

const context: Context = {
    user: { age: 30, isActive: true },
    order: { total: 20, items: [] }
};

// Expected Output (after calling engine.evaluate(context)):
// Action 5 executed

// Explanation:
// The `ConditionEngine` should gracefully handle non-existent keys. When accessing `user.nonExistentProp`, it should return `undefined`.
// The condition `undefined === undefined` evaluates to true.
// The condition `user.age > 0` (30 > 0) also evaluates to true.
// Therefore, the AND condition is true, and Action 5 is executed.

Constraints

  • The depth of nested conditions is unlimited.
  • The Context object can have deeply nested properties, up to 10 levels deep.
  • The number of rules in the engine can be up to 1000.
  • The evaluate method should complete within 500ms for a typical scenario (100 rules, shallow conditions).
  • Input Context values will be primitive types (string, number, boolean), arrays of primitives, null, or undefined.
  • action functions are assumed to be relatively quick and do not perform heavy I/O that would significantly exceed the performance constraint.

Notes

  • Consider how to efficiently access nested properties in the Context. A helper function for path traversal might be useful.
  • Think about the recursive nature of evaluating conditions.
  • Ensure your Condition type is expressive enough to cover all specified logical and comparison operators.
  • For array operations like includes, you will need to handle cases where the Context value at the specified key is not an array.
  • The NOT operator should be applied to a single condition within its conditions array.
  • You might want to create helper functions for evaluating comparison operators and logical operators to keep the main evaluation logic clean.
  • Error handling for invalid condition structures or unsupported operators should be considered, though not explicitly required for the core functionality. You can either throw errors or log warnings.
Loading editor...
typescript