@contextgraph/recommendations - Decision Recommendations Package

Similarity-based decision recommendations for ContextGraph. Analyzes historical decisions to provide intelligent recommendations with risk assessment and confidence scoring.

Table of Contents

Overview

The recommendations package provides:

ComponentPurpose
RecommendationEngineFind similar decisions and generate recommendations
Similarity CalculatorCompare decision contexts with configurable weights
Risk AssessorEvaluate risk based on patterns and precedent outcomes
Feedback SystemTrack accuracy and improve over time

Installation

pnpm add @contextgraph/recommendations

Core Concepts

Decision Context

A decision context captures the key attributes of a decision request:

interface DecisionContext {
  /** Action being requested (e.g., 'create', 'delete', 'approve') */
  action: string;

  /** Type of entity involved (optional) */
  entityType?: string;

  /** Resource path or identifier (optional) */
  resource?: string;

  /** Additional attributes for matching */
  attributes: Record<string, unknown>;

  /** When the decision was made (optional) */
  timestamp?: number;
}

Similarity Matching

Similarity is calculated across four dimensions:

DimensionWeightDescription
Action40%Exact match of action type
Entity Type20%Match of entity type
Resource20%Path similarity (prefix matching)
Attributes20%Jaccard similarity of attributes

Risk Assessment

Risk is evaluated based on:

  1. Action Patterns - Destructive actions (delete, destroy, revoke)
  2. Privilege Patterns - Elevated access (admin, root, sudo)
  3. Precedent Outcomes - Historical failure/rejection rates
  4. Precedent Count - Lack of historical data

API Reference

RecommendationEngine

The main class for generating recommendations.

class RecommendationEngine {
  constructor(
    dtg: DecisionTraceGraph,
    storage: StorageInterface,
    config?: RecommendationEngineConfig
  );

  // Find similar past decisions
  async findSimilarDecisions(context: DecisionContext): Promise<Result<SimilarDecision[]>>;

  // Get a recommendation
  async recommend(context: DecisionContext): Promise<Result<Recommendation>>;

  // Explain a recommendation
  explainRecommendation(recommendationId: string): Result<string>;

  // Submit feedback on a recommendation
  submitFeedback(feedback: RecommendationFeedback): Result<void>;

  // Get statistics
  getStats(): RecommendationStats;

  // Update matching criteria
  setCriteria(criteria: Partial<MatchingCriteria>): void;
}

RecommendationEngineConfig

interface RecommendationEngineConfig {
  /** Matching criteria overrides */
  criteria?: Partial<MatchingCriteria>;

  /** Minimum precedents for confident recommendation (default: 3) */
  minPrecedents?: number;

  /** High-risk action patterns (default: ['delete', 'destroy', 'terminate', 'revoke', 'disable']) */
  highRiskPatterns?: string[];

  /** Escalation-required patterns (default: ['admin', 'root', 'sudo', 'override']) */
  escalationPatterns?: string[];
}

Recommendation

interface Recommendation {
  /** Unique recommendation ID */
  id: string;

  /** Recommended action */
  recommendedAction: 'approve' | 'reject' | 'defer' | 'escalate';

  /** Confidence score (0-1) */
  confidence: number;

  /** Human-readable reasoning */
  reasoning: string;

  /** Similar historical decisions */
  precedents: SimilarDecision[];

  /** Risk assessment */
  risk: RiskAssessment;

  /** When generated */
  generatedAt: number;
}

SimilarDecision

interface SimilarDecision {
  /** Decision ID from DTG */
  decisionId: DecisionId;

  /** Similarity score details */
  similarity: SimilarityScore;

  /** Outcome of the decision */
  outcome: 'approved' | 'rejected' | 'executed' | 'failed';

  /** Age of decision in milliseconds */
  ageMs: number;
}

RiskAssessment

interface RiskAssessment {
  /** Overall risk level */
  level: 'low' | 'medium' | 'high' | 'critical';

  /** Risk score (0-1) */
  score: number;

  /** Individual risk factors */
  factors: RiskFactor[];
}

interface RiskFactor {
  name: string;
  description: string;
  weight: number;
  mitigations?: string[];
}

Example:

import { createRecommendationEngine } from '@contextgraph/recommendations';
import { DecisionTraceGraph } from '@contextgraph/dtg';

const dtg = new DecisionTraceGraph(storage);
const engine = createRecommendationEngine(dtg, storage);

// Get recommendation for a decision
const result = await engine.recommend({
  action: 'delete',
  entityType: 'user',
  resource: '/api/users/123',
  attributes: {
    role: 'admin',
    department: 'engineering',
  },
});

if (result.ok) {
  const rec = result.value;
  console.log(`Recommendation: ${rec.recommendedAction}`);
  console.log(`Confidence: ${(rec.confidence * 100).toFixed(1)}%`);
  console.log(`Reasoning: ${rec.reasoning}`);
  console.log(`Risk Level: ${rec.risk.level}`);
}

calculateSimilarity

Calculate similarity between two decision contexts.

function calculateSimilarity(
  context1: DecisionContext,
  context2: DecisionContext,
  criteria?: MatchingCriteria
): SimilarityScore

SimilarityScore

interface SimilarityScore {
  /** Overall similarity (0-1) */
  score: number;

  /** Breakdown by dimension */
  breakdown: {
    actionMatch: number;
    entityTypeMatch: number;
    resourceMatch: number;
    attributeMatch: number;
  };
}

Example:

import { calculateSimilarity, DEFAULT_CRITERIA } from '@contextgraph/recommendations';

const context1: DecisionContext = {
  action: 'create',
  entityType: 'user',
  resource: '/api/users',
  attributes: { role: 'admin' },
};

const context2: DecisionContext = {
  action: 'create',
  entityType: 'user',
  resource: '/api/users/profile',
  attributes: { role: 'admin', level: 5 },
};

const similarity = calculateSimilarity(context1, context2);

console.log(`Overall: ${similarity.score}`);
console.log(`Action: ${similarity.breakdown.actionMatch}`);       // 1.0 (exact match)
console.log(`Entity: ${similarity.breakdown.entityTypeMatch}`);   // 1.0 (exact match)
console.log(`Resource: ${similarity.breakdown.resourceMatch}`);   // ~0.66 (prefix match)
console.log(`Attributes: ${similarity.breakdown.attributeMatch}`); // ~0.5 (partial overlap)

rankBySimilarity

Rank and filter similar decisions.

function rankBySimilarity<T extends { similarity: SimilarityScore; ageMs: number }>(
  items: T[],
  criteria?: MatchingCriteria
): T[]

Example:

import { rankBySimilarity, DEFAULT_CRITERIA } from '@contextgraph/recommendations';

const decisions = [
  { decisionId: 'a', similarity: { score: 0.7, ... }, ageMs: 1000, outcome: 'approved' },
  { decisionId: 'b', similarity: { score: 0.9, ... }, ageMs: 2000, outcome: 'approved' },
  { decisionId: 'c', similarity: { score: 0.8, ... }, ageMs: 500, outcome: 'rejected' },
];

const ranked = rankBySimilarity(decisions, {
  ...DEFAULT_CRITERIA,
  minSimilarity: 0.5,
  maxPrecedentAge: 7 * 24 * 60 * 60 * 1000, // 7 days
  maxPrecedents: 5,
});

// Returns sorted by score (highest first), filtered by criteria

Configuration

Matching Criteria

interface MatchingCriteria {
  /** Weight for action matching (default: 0.4) */
  actionWeight: number;

  /** Weight for entity type matching (default: 0.2) */
  entityTypeWeight: number;

  /** Weight for resource matching (default: 0.2) */
  resourceWeight: number;

  /** Weight for attribute matching (default: 0.2) */
  attributeWeight: number;

  /** Minimum similarity to consider (default: 0.5) */
  minSimilarity: number;

  /** Maximum age of precedents in ms (default: 90 days) */
  maxPrecedentAge: number;

  /** Maximum number of precedents to return (default: 10) */
  maxPrecedents: number;
}

Default Values:

const DEFAULT_CRITERIA: MatchingCriteria = {
  actionWeight: 0.4,
  entityTypeWeight: 0.2,
  resourceWeight: 0.2,
  attributeWeight: 0.2,
  minSimilarity: 0.5,
  maxPrecedentAge: 90 * 24 * 60 * 60 * 1000, // 90 days
  maxPrecedents: 10,
};

Risk Patterns

Configure which patterns trigger risk flags:

const engine = createRecommendationEngine(dtg, storage, {
  // Actions that increase risk score
  highRiskPatterns: [
    'delete',
    'destroy',
    'terminate',
    'revoke',
    'disable',
    'purge',
    'wipe',
  ],

  // Patterns requiring escalation
  escalationPatterns: [
    'admin',
    'root',
    'sudo',
    'override',
    'bypass',
    'force',
  ],

  // Minimum precedents for confident recommendation
  minPrecedents: 5,
});

Recommendation Types

The engine returns one of four recommendation types:

Approve

Returned when:

  • ≥70% of similar decisions were approved/executed
  • Average similarity ≥60%
  • Risk level is low or medium
{
  recommendedAction: 'approve',
  confidence: 0.85,
  reasoning: '85% of similar decisions were approved. Average similarity: 72%.',
}

Reject

Returned when:

  • ≥60% of similar decisions were rejected/failed
  • Clear pattern of failures
{
  recommendedAction: 'reject',
  confidence: 0.78,
  reasoning: '65% of similar decisions were rejected. Risk factors: High-Risk Action.',
}

Defer

Returned when:

  • Insufficient precedents (below minPrecedents threshold)
  • Mixed results without clear pattern
  • Low confidence in recommendation
{
  recommendedAction: 'defer',
  confidence: 0.6,
  reasoning: 'Insufficient historical precedents (2/5). More context needed.',
}

Escalate

Returned when:

  • Critical risk level detected
  • High risk level with mixed precedent outcomes
  • Escalation patterns detected in action or resource
{
  recommendedAction: 'escalate',
  confidence: 0.9,
  reasoning: 'Critical risk level detected. Manual review by supervisor recommended.',
}

Feedback Loop

Track recommendation accuracy and improve over time:

Submitting Feedback

import type { RecommendationFeedback } from '@contextgraph/recommendations';

const feedback: RecommendationFeedback = {
  recommendationId: 'rec_abc123',
  followed: true,
  actualDecision: 'approved',
  outcome: 'success',
  comments: 'Worked as expected',
  submittedAt: Date.now(),
};

engine.submitFeedback(feedback);

Tracking Statistics

interface RecommendationStats {
  /** Total recommendations generated */
  totalRecommendations: number;

  /** How many recommendations were followed */
  recommendationsFollowed: number;

  /** Average confidence score */
  averageConfidence: number;

  /** Accuracy (correct predictions / total with feedback) */
  accuracy: number;

  /** Distribution of recommendation types */
  decisionDistribution: {
    approve: number;
    reject: number;
    defer: number;
    escalate: number;
  };
}

const stats = engine.getStats();
console.log(`Accuracy: ${(stats.accuracy * 100).toFixed(1)}%`);
console.log(`Average confidence: ${(stats.averageConfidence * 100).toFixed(1)}%`);

Examples

API Request Approval

import { createRecommendationEngine } from '@contextgraph/recommendations';

async function shouldApproveAPIRequest(request: {
  method: string;
  path: string;
  user: { role: string; department: string };
}) {
  const engine = createRecommendationEngine(dtg, storage);

  const recommendation = await engine.recommend({
    action: request.method.toLowerCase(),
    resource: request.path,
    attributes: {
      userRole: request.user.role,
      department: request.user.department,
    },
  });

  if (!recommendation.ok) {
    return { allowed: false, reason: 'Failed to generate recommendation' };
  }

  const rec = recommendation.value;

  switch (rec.recommendedAction) {
    case 'approve':
      return { allowed: true, confidence: rec.confidence };

    case 'reject':
      return { allowed: false, reason: rec.reasoning };

    case 'escalate':
      return { allowed: false, requiresApproval: true, reason: rec.reasoning };

    case 'defer':
    default:
      return { allowed: false, requiresReview: true, reason: rec.reasoning };
  }
}

Workflow Step Recommendation

async function recommendWorkflowAction(
  workflowId: string,
  step: string,
  parameters: Record<string, unknown>
) {
  const engine = createRecommendationEngine(dtg, storage, {
    minPrecedents: 5,
    criteria: {
      actionWeight: 0.3,
      attributeWeight: 0.4, // Higher weight on parameters
    },
  });

  const result = await engine.recommend({
    action: step,
    entityType: 'workflow',
    resource: `/workflows/${workflowId}`,
    attributes: parameters,
  });

  if (!result.ok) {
    return null;
  }

  // Generate explanation
  const explanation = engine.explainRecommendation(result.value.id);

  return {
    recommendation: result.value,
    explanation: explanation.ok ? explanation.value : null,
  };
}

Risk-Aware Batch Processing

async function processBatch(items: Array<{ action: string; target: string }>) {
  const engine = createRecommendationEngine(dtg, storage);
  const results = [];

  for (const item of items) {
    const rec = await engine.recommend({
      action: item.action,
      resource: item.target,
      attributes: {},
    });

    if (!rec.ok) {
      results.push({ item, status: 'error', error: rec.error });
      continue;
    }

    // Auto-approve low-risk items
    if (
      rec.value.recommendedAction === 'approve' &&
      rec.value.risk.level === 'low' &&
      rec.value.confidence >= 0.8
    ) {
      results.push({ item, status: 'auto-approved' });
      continue;
    }

    // Queue high-risk items for manual review
    if (rec.value.risk.level === 'high' || rec.value.risk.level === 'critical') {
      results.push({ item, status: 'manual-review', recommendation: rec.value });
      continue;
    }

    // Default handling
    results.push({ item, status: 'pending', recommendation: rec.value });
  }

  return results;
}

Custom Similarity Weights

// For permission-related decisions, weight action heavily
const permissionEngine = createRecommendationEngine(dtg, storage, {
  criteria: {
    actionWeight: 0.5,    // Most important
    entityTypeWeight: 0.3, // Type of resource matters
    resourceWeight: 0.1,   // Specific path less important
    attributeWeight: 0.1,  // Attributes less important
  },
});

// For data operations, weight resource path heavily
const dataEngine = createRecommendationEngine(dtg, storage, {
  criteria: {
    actionWeight: 0.2,
    entityTypeWeight: 0.1,
    resourceWeight: 0.5,   // Path is critical
    attributeWeight: 0.2,
  },
});

Feedback Integration

class RecommendationService {
  private engine: RecommendationEngine;

  constructor(dtg: DecisionTraceGraph, storage: StorageInterface) {
    this.engine = createRecommendationEngine(dtg, storage);
  }

  async getRecommendation(context: DecisionContext) {
    return this.engine.recommend(context);
  }

  async recordDecisionOutcome(
    recommendationId: string,
    actualDecision: 'approved' | 'rejected',
    wasSuccessful: boolean
  ) {
    const recommendation = this.engine.explainRecommendation(recommendationId);

    if (!recommendation.ok) {
      return;
    }

    // Determine if recommendation was followed
    const wasFollowed =
      (actualDecision === 'approved' && recommendation.value.includes('approve')) ||
      (actualDecision === 'rejected' && recommendation.value.includes('reject'));

    this.engine.submitFeedback({
      recommendationId,
      followed: wasFollowed,
      actualDecision,
      outcome: wasSuccessful ? 'success' : 'failure',
      submittedAt: Date.now(),
    });
  }

  getAccuracyReport() {
    const stats = this.engine.getStats();

    return {
      accuracy: `${(stats.accuracy * 100).toFixed(1)}%`,
      totalRecommendations: stats.totalRecommendations,
      followRate: `${((stats.recommendationsFollowed / stats.totalRecommendations) * 100).toFixed(1)}%`,
      distribution: stats.decisionDistribution,
    };
  }
}

Best Practices

1. Populate Decision History

Recommendations require historical data. Ensure you're recording decisions:

// Record decisions for recommendation training
await dtg.recordDecision({
  type: 'workflow_step',
  title: 'API Request: DELETE /users/123',
  proposedBy: agentId,
  riskLevel: 'medium',
});

// Track outcome
await dtg.executeDecision(decisionId, { success: true });

2. Tune Weights for Your Domain

Different domains need different weight configurations:

// Financial domain - conservative, action-focused
{ actionWeight: 0.5, entityTypeWeight: 0.2, resourceWeight: 0.2, attributeWeight: 0.1 }

// Content management - resource-focused
{ actionWeight: 0.2, entityTypeWeight: 0.2, resourceWeight: 0.4, attributeWeight: 0.2 }

// User management - attribute-focused
{ actionWeight: 0.3, entityTypeWeight: 0.2, resourceWeight: 0.1, attributeWeight: 0.4 }

3. Use Appropriate Confidence Thresholds

// High-stakes decisions
if (rec.confidence < 0.85) {
  // Require manual review
}

// Low-stakes decisions
if (rec.confidence >= 0.7 && rec.risk.level === 'low') {
  // Auto-approve
}

4. Monitor and Improve

// Regular accuracy checks
setInterval(() => {
  const stats = engine.getStats();

  if (stats.accuracy < 0.7) {
    console.warn('Recommendation accuracy dropping');
    // Consider retraining or adjusting criteria
  }
}, 24 * 60 * 60 * 1000); // Daily

5. Handle Edge Cases

async function getRecommendationSafely(context: DecisionContext) {
  const result = await engine.recommend(context);

  if (!result.ok) {
    // Fallback to conservative default
    return {
      recommendedAction: 'defer' as const,
      confidence: 0,
      reasoning: 'Unable to generate recommendation',
      risk: { level: 'medium' as const, score: 0.5, factors: [] },
    };
  }

  // Never auto-approve with no precedents
  if (result.value.precedents.length === 0) {
    return {
      ...result.value,
      recommendedAction: 'defer' as const,
      reasoning: 'No historical precedents available',
    };
  }

  return result.value;
}

6. Attribute Standardization

Ensure consistent attribute naming for better matching:

// Good - consistent naming
{ action: 'create', attributes: { userRole: 'admin', department: 'engineering' } }
{ action: 'create', attributes: { userRole: 'user', department: 'marketing' } }

// Bad - inconsistent naming
{ action: 'create', attributes: { role: 'admin', dept: 'engineering' } }
{ action: 'create', attributes: { userRole: 'user', department: 'marketing' } }