ri-guardrails

User profile synthesis, preference/rejection learning, and three-check blocking middleware


Keywords
guardrails, user-profile, preference-learning, rejection-learning, three-check-blocking, decay, fuzzy-matching, deterministic, typescript
License
MIT
Install
npm install ri-guardrails@1.0.1

Documentation

ri-guardrails

User profile synthesis, preference/rejection learning, and three-check blocking middleware.

CI License: MIT

Overview

ri-guardrails is a standalone TypeScript library that provides:

  • User Profile Synthesis — Builds a compact user profile from event history
  • Preference Learning — Records positive signals with configurable half-life decay
  • Rejection Learning — Persistent rejection memory with hard-blocking at 3+ rejections
  • Three-Check Blocking — Blocks on clarity failure, profile conflict, or feasibility issue
  • Calibration Logging — Records every decision + outcome for profile optimization

Zero runtime dependencies. Pure computation — no I/O, no DOM, no storage.

Getting Started

import { createProfileEngine } from 'ri-guardrails';
import type { ProfileEvent, CheckContext } from 'ri-guardrails';

// 1. Create an engine (deterministic IDs for reproducibility)
let counter = 0;
const engine = createProfileEngine({
  idGenerator: () => `id-${String(++counter)}`,
});

// 2. Synthesize a profile from event history
const events: ProfileEvent[] = [
  {
    id: 'e1', type: 'user_feedback', timestamp: '2024-01-01T00:00:00Z',
    payload: { sentiment: 'positive', category: 'ui_style', value: 'dark_mode' },
  },
  {
    id: 'e2', type: 'user_feedback', timestamp: '2024-01-02T00:00:00Z',
    payload: { sentiment: 'negative', category: 'charts', value: 'pie chart' },
  },
];
const profile = engine.synthesize(events);

// 3. Record additional signals incrementally
engine.recordPreference({
  category: 'ui_style', value: 'dark_mode',
  signalType: 'explicit_approval', timestamp: '2024-01-03T00:00:00Z',
});
engine.recordRejection({
  category: 'charts', pattern: 'pie chart',
  signalType: 'explicit_rejection', timestamp: '2024-01-03T00:00:00Z',
});

// 4. Evaluate a proposed action through the three-check pipeline
const context: CheckContext = {
  clarity: { pass: true, reason: null },
  feasibility: { pass: true, reason: null },
  proposedAction: 'create',
  proposedFeatures: ['bar chart', 'dark mode'],
  currentTimestamp: '2024-01-03T00:00:00Z',
};
const result = engine.evaluate(context);

if (result.action === 'proceed') {
  // Safe to proceed — no conflicts detected
} else {
  // Blocked — check result.blockReason and result.alternatives
  console.log(`Blocked: ${result.blockReason}`);
}

// 5. Record the user's response for calibration
engine.recordOutcome({
  checkResultId: result.calibrationEvent.id,
  userAction: 'accepted',
  timestamp: '2024-01-03T00:00:00Z',
});

Install

npm install ri-guardrails

API

The library exposes a ProfileEngine interface with 7 methods:

interface ProfileEngine {
  synthesize(events: readonly ProfileEvent[]): UserProfile;
  evaluate(context: CheckContext): CheckResult;
  recordPreference(signal: Omit<PreferenceSignal, 'id'>): void;
  recordRejection(signal: Omit<RejectionSignal, 'id'>): void;
  recordOutcome(calibration: CalibrationOutcome): void;
  getConfig(): ProfileConfig;
  updateConfig(patch: Partial<ProfileConfig>): void;
}
Method Purpose
synthesize(events) Build a UserProfile from an event stream
evaluate(context) Run three-check blocking (clarity → profile conflict → feasibility)
recordPreference(signal) Record a positive user signal
recordRejection(signal) Record a negative user signal
recordOutcome(calibration) Log decision outcomes for profile calibration
getConfig() Read current configuration
updateConfig(patch) Update configuration parameters

Types

UserProfile

The synthesized user model — the core output of synthesize().

interface UserProfile {
  readonly preferences: readonly PreferenceEntry[];
  readonly rejections: readonly RejectionEntry[];
  readonly vocabulary: readonly VocabularyMapping[];
  readonly patterns: readonly BehaviorPattern[];
  readonly weights: WeightVector;
  readonly synthesizedAt: string;   // ISO 8601
  readonly eventCount: number;
}

PreferenceEntry

A positive signal — something the user likes.

Field Type Description
id string Unique identifier
category string Domain (e.g., "ui_style", "feature", "layout")
value string What is preferred (e.g., "dark_mode", "grid_layout")
strength number (0–1) Current strength after decay
baseStrength number (0–1) Original strength before decay
firstSeen string ISO 8601 — when first recorded
lastReinforced string ISO 8601 — when last reinforced
occurrences number How many times observed
halfLifeDays number Decay rate (default 90)

RejectionEntry

A negative signal — something the user dislikes. Soft rejections decay; hard blocks (3+ rejections) are permanent.

Field Type Description
id string Unique identifier
category string Domain
pattern string What was rejected (normalized description)
strength number (0–1) Current strength after decay
baseStrength number (0–1) Original strength before decay
firstSeen string ISO 8601
lastReinforced string ISO 8601
occurrences number Total rejection count
isHardBlock boolean true if occurrences ≥ hardBlockThreshold
halfLifeDays number 60 for soft, Infinity for hard blocks

VocabularyMapping

Maps user-provided phrases to system meanings.

Field Type Description
userPhrase string What the user says
systemMeaning string What it maps to
confidence number (0–1) Mapping confidence
lastUsed string ISO 8601
usageCount number Times used

BehaviorPattern

A detected behavioral pattern — frequency-based, with decay.

Field Type Description
id string Unique identifier
description string Human-readable pattern description
frequency number (0–1) How often this pattern occurs
strength number (0–1) After decay
baseStrength number (0–1) Before decay
firstSeen string ISO 8601
lastSeen string ISO 8601
halfLifeDays number Default 120

ProfileEvent

An input event from the caller's domain, used for profile synthesis.

interface ProfileEvent {
  readonly id: string;
  readonly type: string;
  readonly timestamp: string;          // ISO 8601
  readonly payload: Readonly<Record<string, unknown>>;
}

PreferenceSignal & RejectionSignal

Explicit signals for recording user preferences and rejections.

interface PreferenceSignal {
  readonly id: string;
  readonly category: string;
  readonly value: string;
  readonly signalType: PreferenceSignalType;
  readonly timestamp: string;
}

interface RejectionSignal {
  readonly id: string;
  readonly category: string;
  readonly pattern: string;
  readonly signalType: RejectionSignalType;
  readonly timestamp: string;
}

Signal Types & Strength Changes

Signal Type Strength Change Category
'first_attempt_confirm' +0.1 Preference
'explicit_approval' +0.3 Preference
'repeated_usage' +0.2 Preference
'evolve_add' +0.2 Preference
'evolve_remove' +0.2 Rejection
'explicit_rejection' +0.3 Rejection
'repeated_rejection' +0.3 Rejection

CheckContext

Input to the three-check evaluator. Clarity and feasibility are caller-provided.

interface CheckContext {
  readonly clarity: { readonly pass: boolean; readonly reason: string | null };
  readonly feasibility: { readonly pass: boolean; readonly reason: string | null };
  readonly proposedAction: string;
  readonly proposedFeatures: readonly string[];
  readonly currentTimestamp: string;    // ISO 8601 — for decay calculation
}

CheckResult

The result of the three-check evaluation.

interface CheckResult {
  readonly checks: {
    readonly clarity: { readonly pass: boolean; readonly reason: string | null };
    readonly profileConflict: { readonly pass: boolean; readonly conflicts: readonly ConflictDetail[] };
    readonly feasibility: { readonly pass: boolean; readonly reason: string | null };
  };
  readonly action: 'proceed' | 'block';
  readonly blockReason?: 'clarity' | 'profile_conflict' | 'feasibility';
  readonly alternatives?: readonly string[];
  readonly profileSnapshot: UserProfile;
  readonly calibrationEvent: CalibrationEvent;
}

ConflictDetail

Describes a conflict between a proposed action and the user's profile.

Field Type Description
type 'preference_violation' | 'rejection_match' | 'pattern_mismatch' Conflict type
entry PreferenceEntry | RejectionEntry | BehaviorPattern Conflicting profile entry
severity 'soft' | 'hard' Conflict severity
message string Human-readable explanation

CalibrationOutcome & CalibrationEvent

Records user responses to check results for profile optimization.

interface CalibrationOutcome {
  readonly checkResultId: string;
  readonly userAction: 'accepted' | 'rejected' | 'overridden' | 'rephrased';
  readonly timestamp: string;
}

interface CalibrationEvent {
  readonly id: string;
  readonly checkResult: CheckResult;
  readonly outcome: CalibrationOutcome | null;  // null until outcome recorded
}

ProfileConfig

Configuration for the profile engine. All parameters have documented defaults.

interface ProfileConfig {
  readonly preferenceHalfLifeDays: number;        // default: 90
  readonly softRejectionHalfLifeDays: number;     // default: 60
  readonly patternHalfLifeDays: number;           // default: 120
  readonly hardBlockThreshold: number;            // default: 3
  readonly rejectionSimilarityThreshold: number;  // default: 0.7
  readonly softConflictBlockThreshold: number;    // default: 2
  readonly similarityStrategy: SimilarityStrategy; // default: 'jaro-winkler'
}

SimilarityStrategy

type SimilarityStrategy = 'levenshtein' | 'jaro-winkler';

GuardrailsError

Discriminated union for all errors. Switch on code to narrow.

type GuardrailsError =
  | { readonly code: 'INVALID_EVENT'; readonly reason: string }
  | { readonly code: 'INVALID_SIGNAL'; readonly reason: string }
  | { readonly code: 'CONFIG_ERROR'; readonly field: string; readonly reason: string }
  | { readonly code: 'SYNTHESIS_ERROR'; readonly reason: string };

Result<T, E>

Generic result type for fallible operations.

type Result<T, E> =
  | { readonly ok: true; readonly value: T }
  | { readonly ok: false; readonly error: E };

User Profile Structure

Section What It Captures Decay
Preferences Things user likes (features, UI styles, layouts) 90-day half-life
Rejections Things user dislikes Soft: 60-day half-life. Hard (3+ rejections): permanent
Vocabulary User phrases mapped to system meanings No decay
Patterns Behavioral patterns 120-day half-life
Weights Personalized factor importance Derived from patterns

Decay Formula

strength = baseStrength × 0.5^(daysSinceEvent / halfLifeDays)

Decay API

The decay engine is available as standalone functions for direct use:

import {
  computeDecay,
  daysBetween,
  applyDecayToPreference,
  applyDecayToRejection,
  applyDecayToPattern,
} from 'ri-guardrails';
Function Signature Description
computeDecay (baseStrength, daysSinceEvent, halfLifeDays) → Result<number> Core half-life decay formula. Returns decayed strength clamped to [0, 1].
daysBetween (from: string, to: string) → Result<number> Fractional days between two ISO 8601 timestamps. No Date.now().
applyDecayToPreference (entry, currentTimestamp) → PreferenceEntry Returns a new entry with updated strength.
applyDecayToRejection (entry, currentTimestamp) → RejectionEntry Hard-blocked rejections do not decay.
applyDecayToPattern (entry, currentTimestamp) → BehaviorPattern Returns a new entry with updated strength.

Edge cases handled:

  • halfLifeDays === Infinity → no decay (hard blocks)
  • daysSinceEvent === 0 → full strength
  • daysSinceEvent < 0 → returns CONFIG_ERROR
  • halfLifeDays <= 0 → returns CONFIG_ERROR
  • baseStrength outside 0–1 → clamped

Similarity API

Zero-dependency fuzzy text matching for rejection deduplication and conflict detection.

import {
  computeSimilarity,
  findMatchingRejections,
  levenshteinDistance,
  levenshteinSimilarity,
  jaroSimilarity,
  jaroWinklerSimilarity,
  normalize,
  tokenize,
  removePunctuation,
  tokenSortNormalize,
} from 'ri-guardrails';
Function Signature Description
computeSimilarity (a, b, strategy?) → number Strategy-based similarity (0–1). Normalizes + token-sorts inputs. Default: 'jaro-winkler'.
findMatchingRejections (features, rejections, threshold, strategy?) → MatchResult[] Finds rejections matching proposed features above threshold. Sorted by score.
levenshteinDistance (a, b) → number Raw Levenshtein edit distance (integer).
levenshteinSimilarity (a, b) → number Normalized 0–1: 1 - distance/maxLength.
jaroSimilarity (a, b) → number Jaro similarity score (0–1).
jaroWinklerSimilarity (a, b, prefixScale?) → number Jaro-Winkler with prefix bonus. prefixScale default 0.1, max 0.25.
normalize (text) → string Lowercase, trim, collapse spaces.
tokenize (text) → string[] Split normalized text into tokens.
removePunctuation (text) → string Strip non-alphanumeric except whitespace.
tokenSortNormalize (text) → string Normalize + tokenize + sort + rejoin. Handles word order.

Synthesis API

Build a complete UserProfile from event history. Deterministic — same events always produce the same profile.

import {
  synthesize,
  classifyEvent,
  classifyEvents,
  aggregatePreferences,
  aggregateRejections,
  extractVocabulary,
  detectPatterns,
  PREFERENCE_STRENGTH,
  REJECTION_STRENGTH,
} from 'ri-guardrails';

Quick Start

const events: ProfileEvent[] = [
  { id: '1', type: 'user_feedback', timestamp: '2024-01-01T00:00:00Z',
    payload: { sentiment: 'positive', category: 'ui_style', value: 'dark_mode' } },
  { id: '2', type: 'space_created', timestamp: '2024-01-02T00:00:00Z',
    payload: { features: ['grid_layout', 'charts'] } },
  { id: '3', type: 'vocabulary_usage', timestamp: '2024-01-03T00:00:00Z',
    payload: { userPhrase: 'make it pop', systemMeaning: 'increase_contrast' } },
];

const config: ProfileConfig = {
  preferenceHalfLifeDays: 90,
  softRejectionHalfLifeDays: 60,
  patternHalfLifeDays: 120,
  hardBlockThreshold: 3,
  rejectionSimilarityThreshold: 0.7,
  softConflictBlockThreshold: 2,
  similarityStrategy: 'jaro-winkler',
};

const profile = synthesize(events, config);
// profile.preferences — aggregated with decay
// profile.rejections — fuzzy-matched and hard-blocked
// profile.vocabulary — phrase→meaning mappings
// profile.patterns — frequency-based behavioral patterns
// profile.weights — derived from pattern strengths

Functions

Function Signature Description
synthesize (events, config, customClassifiers?) → UserProfile Full pipeline: classify → aggregate → extract → detect → derive weights.
classifyEvent (event, customClassifiers?) → ClassifiedSignal[] Classify a single event into signals. One event can produce multiple signals.
classifyEvents (events, customClassifiers?) → ClassifiedSignal[] Classify all events.
aggregatePreferences (signals, config, currentTimestamp) → PreferenceEntry[] Group by (category, value), merge duplicates, apply decay. Sorted by strength.
aggregateRejections (signals, config, currentTimestamp) → RejectionEntry[] Fuzzy-match grouping, hard block at threshold, decay. Sorted by strength.
extractVocabulary (signals) → VocabularyMapping[] Group by (phrase, meaning), track usage/confidence. No decay.
detectPatterns (signals, totalEventCount, config, currentTimestamp) → BehaviorPattern[] Frequency counting, decay. Sorted by strength.

Signal Strength Constants

const PREFERENCE_STRENGTH = {
  first_attempt_confirm: 0.1,
  explicit_approval: 0.3,
  repeated_usage: 0.2,
  evolve_add: 0.2,
} as const;

const REJECTION_STRENGTH = {
  evolve_remove: 0.2,
  explicit_rejection: 0.3,
  repeated_rejection: 0.3,
} as const;

Event Classification Rules

Event Type Payload Signal
user_feedback sentiment: 'positive' Preference (category + value from payload)
user_feedback sentiment: 'negative' Rejection (category + pattern from payload)
space_created features: string[] Preference per feature + pattern signal
space_evolved action: 'add' Preference signal
space_evolved action: 'remove' Rejection signal
vocabulary_usage userPhrase + systemMeaning Vocabulary signal

Custom classifiers can be passed to synthesize() and classifyEvent() to handle domain-specific event types.

Learning API

Explicit signal recording for incrementally updating user profiles outside of event-based synthesis. All functions are pure — they accept current state and return new state without mutation.

import {
  recordPreference,
  recordRejection,
  recordOutcome,
} from 'ri-guardrails';

Functions

Function Signature Description
recordPreference (entries, signal, config, idGenerator) → PreferenceEntry[] Records or reinforces a preference. Matches by (category, value).
recordRejection (entries, signal, config, idGenerator) → RejectionEntry[] Records or reinforces a rejection with fuzzy matching. Triggers hard block at threshold.
recordOutcome (calEvents, outcome, prefs, rejs) → Result<OutcomeResult> Records user action on a check result. Adjusts strengths based on action.

Determinism Note

All learning functions accept an idGenerator: () => string parameter instead of generating IDs internally. This satisfies the determinism rule — the caller controls all non-deterministic inputs.

Outcome Actions

User Action Effect
'accepted' Strengthens preferences used in the decision (+0.1)
'rejected' Strengthens rejections that caused the block (+0.1)
'overridden' Weakens rejections that caused the block (-0.1)
'rephrased' No strength changes

Three-Check Decision Flow

Checks API

The three-check evaluator runs clarity → profile conflict → feasibility in order, with short-circuiting on failure.

import {
  evaluate,
  checkClarity,
  checkFeasibility,
  checkProfile,
  generateAlternatives,
} from 'ri-guardrails';

Functions

Function Signature Description
evaluate (context, profile, config, calIdGenerator) → CheckResult Main evaluator — runs all three checks in order with short-circuiting.
checkClarity (context) → { pass, reason } Gate 1: clarity check (caller-provided).
checkProfile (context, profile, config) → { pass, conflicts } Gate 2: profile conflict detection using fuzzy matching.
checkFeasibility (context) → { pass, reason } Gate 3: feasibility check (caller-provided).
generateAlternatives (blockReason, conflicts, feasibilityReason, profile) → string[] Generates up to 3 human-readable alternative suggestions when blocked.

Pipeline

evaluate(context) →
  ├─ Clarity (caller-provided) → FAIL → block + ask to rephrase
  │   PASS ↓
  ├─ Profile Conflict (library-computed)
  │   → HARD BLOCK → block + offer override
  │   → SOFT CONFLICT (count ≥ threshold) → block + alternatives
  │   → SOFT CONFLICT (count < threshold) → proceed with warnings
  │   NO CONFLICT ↓
  ├─ Feasibility (caller-provided) → FAIL → block + closest alternative
  │   PASS ↓
  └─ action: 'proceed'

Configuration

Parameter Default Description
preferenceHalfLifeDays 90 How fast preference influence fades
softRejectionHalfLifeDays 60 How fast soft rejection influence fades
patternHalfLifeDays 120 How fast behavioral patterns fade
hardBlockThreshold 3 Rejections needed for permanent block
rejectionSimilarityThreshold 0.7 Fuzzy match threshold for similar rejections
softConflictBlockThreshold 2 Soft conflicts before blocking
similarityStrategy 'jaro-winkler' Fuzzy matching algorithm

Profile Engine

The createProfileEngine factory wires all subsystems into a stateful engine:

import { createProfileEngine } from 'ri-guardrails';

const engine = createProfileEngine({
  config: { preferenceHalfLifeDays: 30 },
  idGenerator: () => crypto.randomUUID(), // caller controls IDs
});

// Full profile synthesis from events
const profile = engine.synthesize(events);

// Incremental updates
engine.recordPreference({ category: 'ui_style', value: 'dark_mode', signalType: 'explicit_approval', timestamp });
engine.recordRejection({ category: 'charts', pattern: 'pie charts', signalType: 'explicit_rejection', timestamp });

// Evaluate proposed actions against the profile
const result = engine.evaluate(context);

// Record user outcome for calibration
engine.recordOutcome({ checkResultId: result.calibrationEvent.id, userAction: 'accepted', timestamp });

// Configuration management
engine.updateConfig({ softConflictBlockThreshold: 3 });
const config = engine.getConfig();
Method Description
synthesize(events) Full profile recomputation from events (replaces current profile)
evaluate(context) Run three-check pipeline against current profile
recordPreference(signal) Incrementally add/reinforce a preference
recordRejection(signal) Incrementally add/reinforce a rejection
recordOutcome(outcome) Record user action on a check result for calibration
getConfig() Get current frozen config
updateConfig(patch) Merge validated config patch (throws on invalid values)

Design Principles

  • Deterministic: Same event history always produces the same profile
  • Pure computation: No I/O, no DOM, no storage — the caller provides all data
  • Zero dependencies: All algorithms (Levenshtein, Jaro-Winkler, decay) implemented from scratch
  • Type-safe: TypeScript strict mode, no any, discriminated union errors

Development

npm run build       # Build ESM + CJS + types
npm run test        # Run tests
npm run test:watch  # Run tests in watch mode
npm run typecheck   # Type check
npm run lint        # Lint
npm run format      # Format with Prettier

Project Status

  • M1: Project Scaffolding (v0.1.0)
  • M2: Core Types & Profile Schema (v0.2.0)
  • M3: Decay Engine (v0.3.0)
  • M8: Fuzzy Matching & Similarity (v0.4.0)
  • M4: Profile Synthesis (v0.5.0)
  • M5: Preference & Rejection Learning (v0.6.0)
  • M6: Three-Check Blocking (v0.7.0)
  • M7: Configuration & Profile Engine Factory (v0.8.0)
  • M9: Integration Tests & Performance Validation (v1.0.0)
  • M10: Documentation & API Polish (v1.0.1)

Documentation

Document Description
API Reference Complete type, function, constant, and error code reference
Architecture Module dependency graph, data flow diagrams, design principles
Integration Guide Install, quick start, configuration, custom classifiers, error handling
Decay System Half-life decay formula, per-type rules, time-series examples, edge cases
Three-Check Blocking Pipeline diagram, gate details, calibration lifecycle, tuning guide

License

MIT