User profile synthesis, preference/rejection learning, and three-check blocking middleware.
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.
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',
});npm install ri-guardrailsThe 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 |
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;
}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) |
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 |
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 |
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 |
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>>;
}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 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 |
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
}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;
}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 |
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
}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'
}type SimilarityStrategy = 'levenshtein' | 'jaro-winkler';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 };Generic result type for fallible operations.
type Result<T, E> =
| { readonly ok: true; readonly value: T }
| { readonly ok: false; readonly error: E };| 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 |
strength = baseStrength × 0.5^(daysSinceEvent / halfLifeDays)
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→ returnsCONFIG_ERROR -
halfLifeDays <= 0→ returnsCONFIG_ERROR -
baseStrengthoutside 0–1 → clamped
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. |
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';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| 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. |
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 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.
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';| 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. |
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.
| 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 |
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';| 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. |
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'
| 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 |
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) |
- 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
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- 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)
| 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 |