ri-guardrails
v1.0.1
Published
User profile synthesis, preference/rejection learning, and three-check blocking middleware
Maintainers
Readme
ri-guardrails
User profile synthesis, preference/rejection learning, and three-check blocking middleware.
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-guardrailsAPI
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 strengthdaysSinceEvent < 0→ returnsCONFIG_ERRORhalfLifeDays <= 0→ returnsCONFIG_ERRORbaseStrengthoutside 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 strengthsFunctions
| 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 PrettierProject Status
- [x] M1: Project Scaffolding (v0.1.0)
- [x] M2: Core Types & Profile Schema (v0.2.0)
- [x] M3: Decay Engine (v0.3.0)
- [x] M8: Fuzzy Matching & Similarity (v0.4.0)
- [x] M4: Profile Synthesis (v0.5.0)
- [x] M5: Preference & Rejection Learning (v0.6.0)
- [x] M6: Three-Check Blocking (v0.7.0)
- [x] M7: Configuration & Profile Engine Factory (v0.8.0)
- [x] M9: Integration Tests & Performance Validation (v1.0.0)
- [x] 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 |
