npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ri-guardrails

v1.0.1

Published

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

Readme

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

  • [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 |

License

MIT