This document provides context for AI assistants (like Claude) working on the Experience SDK project.
Experience SDK is a lightweight, explainable, plugin-based client-side experience runtime built on @lytics/sdk-kit.
Core Differentiator: Explainability-first - every decision returns structured reasons for why an experience was shown or hidden.
Key Goals:
- Showcase sdk-kit as a foundation for building SDKs
- Enable developers to understand "why" decisions are made
- Support both script tag (IIFE) and npm/bundler (ESM) usage
- Serve as foundation for future projects (Chrome extension, etc.)
We follow a deliberate, specification-first approach inspired by github/spec-kit:
Research → Plan → Implement → Validate
- Research (
notes/- gitignored): Personal exploration, API research, brainstorming - Planning (
specs/- committed): Formal specifications, implementation plans, task breakdowns - Implementation: Follow the plan systematically
- Validation: Tests pass, linter clean, acceptance criteria met
Key Files:
specs/phase-X-name/spec.md- What we're buildingspecs/phase-X-name/plan.md- How we're building it (with code examples)specs/phase-X-name/tasks.md- GitHub-ready issuesspecs/phase-X-name/contracts/- Type definitions
Before building anything, check if it already exists in sdk-kit:
✅ Available in sdk-kit (USE THESE):
- Core:
SDK,Emitter,Config,Namespace,Expose - Plugins:
storagePlugin,contextPlugin,queuePlugin,transportPlugin,consentPlugin,pollPlugin
🔨 Build Only What's Missing:
frequencyPlugin- Experience frequency capping (uses sdk-kit storage underneath)debugPlugin- Window event emission for Chrome extension integrationbannerPlugin- DOM rendering for banner experiences
💡 Design for Contribution: If we build something generic, design it to potentially contribute back to sdk-kit.
Prefer pure functions over complex stateful objects:
// ✅ Good - Pure function, easy to test
function evaluateUrlRule(rule: UrlRule, url: string): boolean {
if (rule.equals) return url === rule.equals;
if (rule.contains) return url.includes(rule.contains);
if (rule.matches) return rule.matches.test(url);
return true;
}
// ❌ Avoid - Hard to test, tightly coupled
class RuleEvaluator {
constructor(private context: Context) {}
evaluate() {
// Complex stateful logic
}
}Benefits:
- Easy to unit test
- No mocking required
- Composable and reusable
- Clear inputs and outputs
Every decision must include human-readable reasons:
interface Decision {
show: boolean;
experienceId?: string;
reasons: string[]; // ["✅ URL matches", "❌ Frequency cap reached"]
trace: TraceStep[]; // Machine-readable trace
context: Context; // Full input context
metadata: DecisionMetadata;
}-
Check the spec first:
- Read
specs/phase-0-foundation/spec.mdfor goals - Read
specs/phase-0-foundation/plan.mdfor implementation details - Check
specs/phase-0-foundation/tasks.mdfor task breakdown
- Read
-
Check what exists:
- Review
/Users/prosseng/workspace/sdk-kitfor available capabilities - Don't reinvent - use sdk-kit where possible
- Review
-
Follow the plan:
- Implementation order matters (dependencies!)
- Use code examples in
plan.mdas starting point - Extract logic into pure functions
-
Write tests alongside:
- Unit tests for all functions
- Aim for >80% coverage
- Test happy path + edge cases
-
Start with types (
types.ts):- Define interfaces first
- Use strict TypeScript
- Export all public types
-
Extract pure functions:
- Business logic = pure functions
- Side effects = minimal, isolated
- Makes testing trivial
-
Follow sdk-kit patterns for plugins:
export const myPlugin: PluginFunction = (plugin, instance, config) => { plugin.ns('my.plugin'); plugin.defaults({ my: { setting: 'value' } }); plugin.expose({ myMethod() {} }); instance.on('event', () => {}); };
-
Document decisions:
- Update spec if deviating from plan
- Add code comments for complex logic
- Update README for new features
DO:
- ✅ Follow conventional commits:
feat:,fix:,docs:,chore: - ✅ Run
pnpm build && pnpm test && pnpm lintbefore committing - ✅ Update specs if implementation differs from plan
- ✅ Extract testable pure functions
- ✅ Include acceptance criteria in commits
- ✅ Use sdk-kit capabilities where available
- ✅ Use
anyintentionally in public APIs (like sdk-kit does)
DON'T:
- ❌ Commit without running tests and linter
- ❌ Skip type definitions for public APIs
- ❌ Build what already exists in sdk-kit
- ❌ Create complex stateful classes when pure functions suffice
- ❌ Use
anyin internal implementation (only in public APIs) - ❌ Commit changes without updating relevant specs
About any Types:
Following sdk-kit's pattern, any is intentionally used in public API files (types.ts) for:
- Config values, event payloads, custom user data
- Better developer experience and API flexibility
- Biome configured to allow
anyin specific files only - Internal implementation should use specific types
experience-sdk/
├── notes/ # Personal research (gitignored)
│ ├── project-scope.md
│ ├── vision-and-roadmap.md
│ └── IMPLEMENTATION_PLAN.md (consolidated)
├── specs/ # Formal specifications (committed)
│ └── phase-0-foundation/
│ ├── spec.md # Goals, scope, user stories
│ ├── plan.md # Step-by-step implementation
│ ├── tasks.md # 14 GitHub-ready issues
│ └── contracts/ # Type contracts
├── packages/
│ ├── core/ # @prosdevlab/experience-sdk
│ │ └── src/
│ │ ├── types.ts # All TypeScript types
│ │ ├── runtime.ts # ExperienceRuntime class
│ │ └── index.ts # Exports (singleton + instance)
│ └── plugins/ # @prosdevlab/experience-sdk-plugins
│ └── src/
│ ├── frequency/ # Frequency capping (uses sdk-kit storage)
│ ├── debug/ # Debug/logging
│ └── banner/ # Banner rendering
├── demo/ # Interactive demo site
├── .cursorrules # Cursor IDE rules
├── DEVELOPMENT.md # Workflow documentation
└── CLAUDE.md # This file
Phase 0: Foundation (In Progress)
- ✅ Project setup (monorepo, packages, CI/CD)
- ✅ Specs written (spec.md, plan.md, tasks.md)
- ✅ GitHub labels and milestone created
- 🚧 Implementation (14 tasks)
Next Steps:
- Create 14 GitHub issues from
tasks.md - Implement Task 1.1: Define TypeScript types
- Follow plan.md order for remaining tasks
specs/phase-0-foundation/plan.md- Detailed code examplesspecs/phase-0-foundation/contracts/types.ts- Type contracts/Users/prosseng/workspace/sdk-kit- Available sdk-kit capabilities
specs/phase-0-foundation/spec.md- Goals and scopenotes/IMPLEMENTATION_PLAN.md- Consolidated decisions.cursorrules- Code standards and guidelines
DEVELOPMENT.md- Spec-driven workflow guidespecs/phase-0-foundation/tasks.md- Task breakdown
// Singleton (script tag)
experiences.init({ debug: true });
experiences.register('welcome', { ... });
// Instance (npm/bundler)
import { createInstance } from '@prosdevlab/experience-sdk';
const exp = createInstance({ debug: true });// Extract evaluation logic into pure functions
function evaluateTargeting(
experience: Experience,
context: Context
): { matched: boolean; reasons: string[] } {
const reasons: string[] = [];
let matched = true;
if (experience.targeting.url) {
const urlMatch = evaluateUrlRule(experience.targeting.url, context.url);
matched = matched && urlMatch;
reasons.push(urlMatch ? '✅ URL matches' : '❌ URL does not match');
}
return { matched, reasons };
}export const frequencyPlugin: PluginFunction = (plugin, instance, config) => {
plugin.ns('experiences.frequency');
plugin.defaults({ frequency: { enabled: true } });
// Use sdk-kit's storage plugin
if (!instance.storage) {
instance.use(storagePlugin);
}
// Expose API
plugin.expose({
frequency: {
getImpressionCount: () => {},
recordImpression: () => {},
},
});
// React to events
instance.on('experiences:evaluated', (decision) => {
// Frequency logic
});
};Goal: >80% coverage with meaningful tests
// Unit test pure functions
describe('evaluateUrlRule', () => {
it('should match URL with contains rule', () => {
const rule = { contains: '/products' };
const url = 'https://example.com/products/123';
expect(evaluateUrlRule(rule, url)).toBe(true);
});
it('should not match URL without contains rule', () => {
const rule = { contains: '/products' };
const url = 'https://example.com/about';
expect(evaluateUrlRule(rule, url)).toBe(false);
});
});Author:
- Name:
prosdevlab - Email:
prosdevlab@gmail.com
Commit Format:
git commit -m "feat: add URL rule evaluation
- Implement evaluateUrlRule function
- Support contains, equals, matches patterns
- Add unit tests with 100% coverage
Closes #5"When implementing, consider:
- Does sdk-kit already provide this? → Check
/Users/prosseng/workspace/sdk-kit - Is this logic testable? → Extract into pure function if not
- Does this match the plan? → Review
plan.mdcode examples - Is this explainable? → Add human-readable reasons
- Are types defined? → Add to
types.tsfirst - Will this work with both singleton and instance API? → Test both patterns
- SDK Kit: https://github.com/Lytics/sdk-kit
- Spec Kit: https://github.com/github/spec-kit
- Our Specs:
specs/phase-0-foundation/ - Implementation Plan:
specs/phase-0-foundation/plan.md
- Read the spec before coding - It has all the answers
- Extract logic into pure functions - Makes everything testable
- Use sdk-kit capabilities - Don't reinvent the wheel
- Write tests alongside code - Not after
- Update specs when deviating - Keep documentation current
- Ask questions early - Don't guess, clarify
Remember: This project values deliberate design over speed. Take time to understand the spec, leverage existing tools, and write testable code. The spec-driven workflow ensures we build the right thing, the right way.