From d9e4eba82ad6d39fee0ed5ef7522239f78f9fd5f Mon Sep 17 00:00:00 2001 From: prosdevlab Date: Wed, 24 Dec 2025 20:13:40 -0800 Subject: [PATCH] feat: integrate plugins into core runtime - Auto-register storage, debug, frequency, and banner plugins in ExperienceRuntime constructor - Move shared types to plugins package to resolve circular dependency - Update core to depend on plugins package - Bundle all dependencies in IIFE build for script tag usage - Remove default export to fix named/default export warning - Configure Biome to allow any in specs contracts - Update pre-commit hook to only lint staged files (matches sdk-kit pattern) All plugins now work seamlessly with the runtime. Closes #9 --- .changeset/config.json | 2 +- .husky/pre-commit | 18 +++- biome.json | 6 +- commitlint.config.js | 8 +- package.json | 2 +- packages/core/package.json | 6 +- packages/core/src/index.ts | 53 +++++------ packages/core/src/runtime.test.ts | 95 +++++++++---------- packages/core/src/runtime.ts | 9 +- packages/core/src/singleton.test.ts | 9 +- packages/core/src/singleton.ts | 3 +- packages/core/src/types.ts | 1 - packages/core/tsconfig.json | 2 +- packages/core/tsup.config.ts | 4 +- packages/plugins/package.json | 5 +- packages/plugins/src/banner/banner.test.ts | 9 +- packages/plugins/src/banner/banner.ts | 3 +- packages/plugins/src/banner/index.ts | 1 - packages/plugins/src/debug/debug.test.ts | 15 +-- packages/plugins/src/debug/debug.ts | 1 - packages/plugins/src/debug/index.ts | 1 - .../plugins/src/frequency/frequency.test.ts | 9 +- packages/plugins/src/frequency/frequency.ts | 15 ++- packages/plugins/src/frequency/index.ts | 1 - packages/plugins/src/index.ts | 15 ++- packages/plugins/src/types.ts | 80 ++++++++++++++++ packages/plugins/tsconfig.json | 2 +- pnpm-lock.yaml | 11 ++- specs/phase-0-foundation/contracts/types.ts | 14 +-- tsconfig.json | 2 +- turbo.json | 2 +- vitest.config.ts | 4 +- 32 files changed, 248 insertions(+), 160 deletions(-) create mode 100644 packages/plugins/src/types.ts diff --git a/.changeset/config.json b/.changeset/config.json index beeeaee..f26daf1 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -8,4 +8,4 @@ "baseBranch": "main", "updateInternalDependencies": "patch", "ignore": [] -} \ No newline at end of file +} diff --git a/.husky/pre-commit b/.husky/pre-commit index 2bda1b8..0c3f594 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -3,12 +3,24 @@ echo "🔍 Running pre-commit checks..." -# Lint and format staged files -echo "📝 Linting and formatting..." -pnpm lint-staged +# Build packages first (needed for typecheck due to workspace dependencies) +echo "🔨 Building packages..." +pnpm build > /dev/null 2>&1 + +# Get staged files +files=$(git diff --cached --name-only --diff-filter=ACMR "*.ts" "*.tsx" "*.js" "*.jsx" | xargs) + +if [ -n "$files" ]; then + echo "📝 Linting and formatting staged files..." + pnpm lint-staged $files + git add $files +fi # Type check entire project echo "🔎 Type checking..." pnpm typecheck +# Clean up dist files (they shouldn't be committed) +rm -rf packages/*/dist + echo "✅ Pre-commit checks passed!" diff --git a/biome.json b/biome.json index 72378bf..c4010bd 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.0/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json", "assist": { "actions": { "source": { "organizeImports": "on" } } }, "linter": { "enabled": true, @@ -30,7 +30,9 @@ { "includes": [ "packages/core/src/types.ts", - "packages/core/src/runtime.ts" + "packages/core/src/runtime.ts", + "packages/plugins/src/types.ts", + "specs/**/contracts/types.ts" ], "linter": { "rules": { diff --git a/commitlint.config.js b/commitlint.config.js index a50e65f..45f8399 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -2,10 +2,6 @@ module.exports = { extends: ['@commitlint/config-conventional'], rules: { 'body-max-line-length': [2, 'always', 100], - 'subject-case': [ - 2, - 'never', - ['start-case', 'pascal-case', 'upper-case'], - ], + 'subject-case': [2, 'never', ['start-case', 'pascal-case', 'upper-case']], }, -}; \ No newline at end of file +}; diff --git a/package.json b/package.json index d343ce9..4217880 100644 --- a/package.json +++ b/package.json @@ -52,4 +52,4 @@ "dependencies": { "@lytics/sdk-kit-plugins": "0.1.2" } -} \ No newline at end of file +} diff --git a/packages/core/package.json b/packages/core/package.json index 8c6781b..b284622 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -42,7 +42,9 @@ "test:watch": "vitest" }, "dependencies": { - "@lytics/sdk-kit": "^0.1.1" + "@lytics/sdk-kit": "^0.1.1", + "@lytics/sdk-kit-plugins": "^0.1.2", + "@prosdevlab/experience-sdk-plugins": "workspace:*" }, "devDependencies": { "@types/node": "^24.0.0", @@ -50,4 +52,4 @@ "typescript": "^5.9.3", "vitest": "^4.0.16" } -} \ No newline at end of file +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8e7bee4..1ad57ff 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -5,31 +5,13 @@ * built on @lytics/sdk-kit. */ -// Export all types -export type { - Experience, - TargetingRules, - UrlRule, - FrequencyRule, - FrequencyConfig, - ExperienceContent, - BannerContent, - ModalContent, - TooltipContent, - ModalAction, - Context, - UserContext, - Decision, - TraceStep, - DecisionMetadata, - ExperienceConfig, - RuntimeState, -} from './types'; +// Re-export plugins for convenience +export { bannerPlugin, debugPlugin, frequencyPlugin } from '@prosdevlab/experience-sdk-plugins'; // Export runtime class and functions export { - ExperienceRuntime, buildContext, + ExperienceRuntime, evaluateExperience, evaluateUrlRule, } from './runtime'; @@ -37,14 +19,31 @@ export { // Export singleton API export { createInstance, - init, - register, + destroy, evaluate, explain, getState, + init, on, - destroy, - experiences as default, + register, } from './singleton'; - - +// Export all types +export type { + BannerContent, + Context, + Decision, + DecisionMetadata, + Experience, + ExperienceConfig, + ExperienceContent, + FrequencyConfig, + FrequencyRule, + ModalAction, + ModalContent, + RuntimeState, + TargetingRules, + TooltipContent, + TraceStep, + UrlRule, + UserContext, +} from './types'; diff --git a/packages/core/src/runtime.test.ts b/packages/core/src/runtime.test.ts index 90f96f6..87fd6fe 100644 --- a/packages/core/src/runtime.test.ts +++ b/packages/core/src/runtime.test.ts @@ -93,17 +93,17 @@ describe('ExperienceRuntime', () => { }); it('should allow multiple experiences', () => { - runtime.register('exp1', { - type: 'banner', - targeting: {}, - content: { title: 'Exp 1', message: 'Message 1' }, - }); + runtime.register('exp1', { + type: 'banner', + targeting: {}, + content: { title: 'Exp 1', message: 'Message 1' }, + }); - runtime.register('exp2', { - type: 'banner', - targeting: {}, - content: { title: 'Exp 2', message: 'Message 2' }, - }); + runtime.register('exp2', { + type: 'banner', + targeting: {}, + content: { title: 'Exp 2', message: 'Message 2' }, + }); const state = runtime.getState(); expect(state.experiences.size).toBe(2); @@ -112,13 +112,13 @@ describe('ExperienceRuntime', () => { describe('evaluate()', () => { beforeEach(() => { - runtime.register('test', { - type: 'banner', - targeting: { - url: { contains: '/products' }, - }, - content: { title: 'Test', message: 'Test message' }, - }); + runtime.register('test', { + type: 'banner', + targeting: { + url: { contains: '/products' }, + }, + content: { title: 'Test', message: 'Test message' }, + }); }); it('should return decision with matched experience', () => { @@ -199,13 +199,13 @@ describe('ExperienceRuntime', () => { }); it('should match first experience only', () => { - runtime.register('test2', { - type: 'banner', - targeting: { - url: { contains: '/products' }, - }, - content: { title: 'Test 2', message: 'Test 2 message' }, - }); + runtime.register('test2', { + type: 'banner', + targeting: { + url: { contains: '/products' }, + }, + content: { title: 'Test 2', message: 'Test 2 message' }, + }); const decision = runtime.evaluate({ url: 'https://example.com/products', @@ -218,13 +218,13 @@ describe('ExperienceRuntime', () => { describe('explain()', () => { it('should explain specific experience', () => { - runtime.register('test', { - type: 'banner', - targeting: { - url: { contains: '/test' }, - }, - content: { title: 'Test', message: 'Test message' }, - }); + runtime.register('test', { + type: 'banner', + targeting: { + url: { contains: '/test' }, + }, + content: { title: 'Test', message: 'Test message' }, + }); const explanation = runtime.explain('test'); @@ -286,12 +286,12 @@ describe('ExperienceRuntime', () => { describe('destroy()', () => { it('should clean up runtime', async () => { - await runtime.init(); - runtime.register('test', { - type: 'banner', - targeting: {}, - content: { title: 'Test', message: 'Test message' }, - }); + await runtime.init(); + runtime.register('test', { + type: 'banner', + targeting: {}, + content: { title: 'Test', message: 'Test message' }, + }); await runtime.destroy(); @@ -398,26 +398,22 @@ describe('ExperienceRuntime', () => { describe('evaluateUrlRule', () => { it('should match with equals rule', () => { - expect(evaluateUrlRule({ equals: 'https://example.com' }, 'https://example.com')).toBe( - true - ); + expect(evaluateUrlRule({ equals: 'https://example.com' }, 'https://example.com')).toBe(true); expect(evaluateUrlRule({ equals: 'https://example.com' }, 'https://other.com')).toBe(false); }); it('should match with contains rule', () => { - expect(evaluateUrlRule({ contains: '/products' }, 'https://example.com/products')).toBe( - true - ); + expect(evaluateUrlRule({ contains: '/products' }, 'https://example.com/products')).toBe(true); expect(evaluateUrlRule({ contains: '/products' }, 'https://example.com/about')).toBe(false); }); it('should match with regex rule', () => { - expect(evaluateUrlRule({ matches: /\/product\/\d+/ }, 'https://example.com/product/123')).toBe( - true - ); - expect(evaluateUrlRule({ matches: /\/product\/\d+/ }, 'https://example.com/product/abc')).toBe( - false - ); + expect( + evaluateUrlRule({ matches: /\/product\/\d+/ }, 'https://example.com/product/123') + ).toBe(true); + expect( + evaluateUrlRule({ matches: /\/product\/\d+/ }, 'https://example.com/product/abc') + ).toBe(false); }); it('should return true for empty rule', () => { @@ -465,4 +461,3 @@ describe('ExperienceRuntime', () => { }); }); }); - diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts index 6b4a827..e69ea17 100644 --- a/packages/core/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -1,4 +1,6 @@ import { SDK } from '@lytics/sdk-kit'; +import { storagePlugin } from '@lytics/sdk-kit-plugins'; +import { bannerPlugin, debugPlugin, frequencyPlugin } from '@prosdevlab/experience-sdk-plugins'; import type { Context, Decision, @@ -32,6 +34,12 @@ export class ExperienceRuntime { name: 'experience-sdk', ...config, }); + + // Auto-register plugins + this.sdk.use(storagePlugin); + this.sdk.use(debugPlugin); + this.sdk.use(frequencyPlugin); + this.sdk.use(bannerPlugin); } /** @@ -263,4 +271,3 @@ export function evaluateUrlRule(rule: UrlRule, url: string = ''): boolean { // No rules specified = match all return true; } - diff --git a/packages/core/src/singleton.test.ts b/packages/core/src/singleton.test.ts index 282bcb8..6b1338a 100644 --- a/packages/core/src/singleton.test.ts +++ b/packages/core/src/singleton.test.ts @@ -1,14 +1,14 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { createInstance, - init, - register, + destroy, evaluate, + experiences as experiencesDefault, explain, getState, + init, on, - destroy, - experiences as experiencesDefault, + register, } from './singleton'; describe('Export Pattern', () => { @@ -234,4 +234,3 @@ describe('Export Pattern', () => { }); }); }); - diff --git a/packages/core/src/singleton.ts b/packages/core/src/singleton.ts index 96e53ed..f6420d7 100644 --- a/packages/core/src/singleton.ts +++ b/packages/core/src/singleton.ts @@ -6,7 +6,7 @@ */ import { ExperienceRuntime } from './runtime'; -import type { ExperienceConfig, Experience, Context, Decision, RuntimeState } from './types'; +import type { Context, Decision, Experience, ExperienceConfig, RuntimeState } from './types'; /** * Create a new Experience SDK instance @@ -174,4 +174,3 @@ export const experiences = { if (typeof window !== 'undefined') { (window as unknown as Record).experiences = experiences; } - diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 4e6f8e1..e2fb2c1 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -251,4 +251,3 @@ export interface RuntimeState { /** Current configuration */ config: ExperienceConfig; } - diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 8a34969..8b411f1 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -6,4 +6,4 @@ }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts"] -} \ No newline at end of file +} diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts index 1fd4153..0744fd7 100644 --- a/packages/core/tsup.config.ts +++ b/packages/core/tsup.config.ts @@ -11,6 +11,6 @@ export default defineConfig({ minify: true, outDir: 'dist', globalName: 'experiences', - // Bundle sdk-kit for IIFE (script tag) - noExternal: ['@lytics/sdk-kit'], + // Bundle dependencies for IIFE (script tag) + noExternal: ['@lytics/sdk-kit', '@lytics/sdk-kit-plugins', '@prosdevlab/experience-sdk-plugins'], }); diff --git a/packages/plugins/package.json b/packages/plugins/package.json index 789683b..1e55d9b 100644 --- a/packages/plugins/package.json +++ b/packages/plugins/package.json @@ -24,12 +24,11 @@ }, "dependencies": { "@lytics/sdk-kit": "^0.1.1", - "@lytics/sdk-kit-plugins": "^0.1.0", - "@prosdevlab/experience-sdk": "workspace:*" + "@lytics/sdk-kit-plugins": "^0.1.2" }, "devDependencies": { "tsup": "^8.5.1", "typescript": "^5.9.3", "vitest": "^4.0.16" } -} \ No newline at end of file +} diff --git a/packages/plugins/src/banner/banner.test.ts b/packages/plugins/src/banner/banner.test.ts index fca437e..f55804b 100644 --- a/packages/plugins/src/banner/banner.test.ts +++ b/packages/plugins/src/banner/banner.test.ts @@ -1,7 +1,7 @@ -import { beforeEach, describe, expect, it, vi, afterEach } from 'vitest'; import { SDK } from '@lytics/sdk-kit'; -import { bannerPlugin, type BannerPlugin } from './banner'; -import type { Experience } from '@prosdevlab/experience-sdk'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { Experience } from '../types'; +import { type BannerPlugin, bannerPlugin } from './banner'; type SDKWithBanner = SDK & { banner: BannerPlugin }; @@ -356,7 +356,7 @@ describe('Banner Plugin', () => { expect(document.querySelector('[data-experience-id="test-banner"]')).toBeTruthy(); await sdk.destroy(); - + // After destroy, the banner should be removed from DOM expect(document.querySelector('[data-experience-id="test-banner"]')).toBeNull(); }); @@ -436,4 +436,3 @@ describe('Banner Plugin', () => { }); }); }); - diff --git a/packages/plugins/src/banner/banner.ts b/packages/plugins/src/banner/banner.ts index 2a19b5f..ee8883b 100644 --- a/packages/plugins/src/banner/banner.ts +++ b/packages/plugins/src/banner/banner.ts @@ -6,7 +6,7 @@ */ import type { PluginFunction } from '@lytics/sdk-kit'; -import type { Experience, BannerContent, Decision } from '@prosdevlab/experience-sdk'; +import type { BannerContent, Decision, Experience } from '../types'; export interface BannerPluginConfig { banner?: { @@ -211,4 +211,3 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => { remove(); }); }; - diff --git a/packages/plugins/src/banner/index.ts b/packages/plugins/src/banner/index.ts index 5565e74..68abde5 100644 --- a/packages/plugins/src/banner/index.ts +++ b/packages/plugins/src/banner/index.ts @@ -4,4 +4,3 @@ export type { BannerPlugin, BannerPluginConfig } from './banner'; export { bannerPlugin } from './banner'; - diff --git a/packages/plugins/src/debug/debug.test.ts b/packages/plugins/src/debug/debug.test.ts index df0ebd9..e7feb66 100644 --- a/packages/plugins/src/debug/debug.test.ts +++ b/packages/plugins/src/debug/debug.test.ts @@ -1,6 +1,6 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; import { SDK } from '@lytics/sdk-kit'; -import { debugPlugin, type DebugPlugin } from './debug'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { type DebugPlugin, debugPlugin } from './debug'; describe('Debug Plugin', () => { let sdk: SDK & { debug: DebugPlugin }; @@ -129,10 +129,7 @@ describe('Debug Plugin', () => { sdk.use(debugPlugin); sdk.emit('experiences:ready'); - expect(consoleSpy).toHaveBeenCalledWith( - '[experiences] SDK initialized and ready', - '' - ); + expect(consoleSpy).toHaveBeenCalledWith('[experiences] SDK initialized and ready', ''); consoleSpy.mockRestore(); }); @@ -144,10 +141,7 @@ describe('Debug Plugin', () => { const payload = { id: 'test', experience: { type: 'banner' } }; sdk.emit('experiences:registered', payload); - expect(consoleSpy).toHaveBeenCalledWith( - '[experiences] Experience registered', - payload - ); + expect(consoleSpy).toHaveBeenCalledWith('[experiences] Experience registered', payload); consoleSpy.mockRestore(); }); @@ -234,4 +228,3 @@ describe('Debug Plugin', () => { }); }); }); - diff --git a/packages/plugins/src/debug/debug.ts b/packages/plugins/src/debug/debug.ts index ca1a19f..5a2e3f3 100644 --- a/packages/plugins/src/debug/debug.ts +++ b/packages/plugins/src/debug/debug.ts @@ -104,4 +104,3 @@ export const debugPlugin: PluginFunction = (plugin, instance, config) => { }); } }; - diff --git a/packages/plugins/src/debug/index.ts b/packages/plugins/src/debug/index.ts index 0dbf4dc..34162b7 100644 --- a/packages/plugins/src/debug/index.ts +++ b/packages/plugins/src/debug/index.ts @@ -4,4 +4,3 @@ export type { DebugPlugin, DebugPluginConfig } from './debug'; export { debugPlugin } from './debug'; - diff --git a/packages/plugins/src/frequency/frequency.test.ts b/packages/plugins/src/frequency/frequency.test.ts index 82523d7..6f9c45b 100644 --- a/packages/plugins/src/frequency/frequency.test.ts +++ b/packages/plugins/src/frequency/frequency.test.ts @@ -1,8 +1,8 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; import { SDK } from '@lytics/sdk-kit'; -import { storagePlugin, type StoragePlugin } from '@lytics/sdk-kit-plugins'; -import { frequencyPlugin, type FrequencyPlugin } from './frequency'; -import type { Decision } from '@prosdevlab/experience-sdk'; +import { type StoragePlugin, storagePlugin } from '@lytics/sdk-kit-plugins'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { Decision } from '../types'; +import { type FrequencyPlugin, frequencyPlugin } from './frequency'; type SDKWithFrequency = SDK & { frequency: FrequencyPlugin; storage: StoragePlugin }; @@ -349,4 +349,3 @@ describe('Frequency Plugin', () => { }); }); }); - diff --git a/packages/plugins/src/frequency/frequency.ts b/packages/plugins/src/frequency/frequency.ts index 81b3dcc..61aaf3b 100644 --- a/packages/plugins/src/frequency/frequency.ts +++ b/packages/plugins/src/frequency/frequency.ts @@ -6,8 +6,8 @@ */ import type { PluginFunction, SDK } from '@lytics/sdk-kit'; -import { storagePlugin, type StoragePlugin } from '@lytics/sdk-kit-plugins'; -import type { Decision } from '@prosdevlab/experience-sdk'; +import { type StoragePlugin, storagePlugin } from '@lytics/sdk-kit-plugins'; +import type { Decision } from '../types'; export interface FrequencyPluginConfig { frequency?: { @@ -71,7 +71,7 @@ export const frequencyPlugin: PluginFunction = (plugin, instance, config) => { const getImpressionData = (experienceId: string): ImpressionData => { const storage = (instance as SDK & { storage: StoragePlugin }).storage; const data = storage.get(getStorageKey(experienceId)) as ImpressionData | null; - + if (!data) { return { count: 0, @@ -79,7 +79,7 @@ export const frequencyPlugin: PluginFunction = (plugin, instance, config) => { impressions: [], }; } - + return data; }; @@ -119,7 +119,7 @@ export const frequencyPlugin: PluginFunction = (plugin, instance, config) => { per: 'session' | 'day' | 'week' ): boolean => { if (!isEnabled()) return false; - + const data = getImpressionData(experienceId); const timeWindow = getTimeWindow(per); const now = Date.now(); @@ -130,9 +130,7 @@ export const frequencyPlugin: PluginFunction = (plugin, instance, config) => { } // For time-based caps, count impressions within the window - const recentImpressions = data.impressions.filter( - (timestamp) => now - timestamp < timeWindow - ); + const recentImpressions = data.impressions.filter((timestamp) => now - timestamp < timeWindow); return recentImpressions.length >= max; }; @@ -185,4 +183,3 @@ export const frequencyPlugin: PluginFunction = (plugin, instance, config) => { }); } }; - diff --git a/packages/plugins/src/frequency/index.ts b/packages/plugins/src/frequency/index.ts index a01b639..358a920 100644 --- a/packages/plugins/src/frequency/index.ts +++ b/packages/plugins/src/frequency/index.ts @@ -4,4 +4,3 @@ export type { FrequencyPlugin, FrequencyPluginConfig } from './frequency'; export { frequencyPlugin } from './frequency'; - diff --git a/packages/plugins/src/index.ts b/packages/plugins/src/index.ts index fe90bd0..c54eb1d 100644 --- a/packages/plugins/src/index.ts +++ b/packages/plugins/src/index.ts @@ -4,6 +4,19 @@ * Official plugins for Experience SDK */ +export * from './banner'; + +// Export plugins export * from './debug'; export * from './frequency'; -export * from './banner'; +// Export shared types +export type { + BannerContent, + Decision, + DecisionMetadata, + Experience, + ExperienceContent, + ModalContent, + TooltipContent, + TraceStep, +} from './types'; diff --git a/packages/plugins/src/types.ts b/packages/plugins/src/types.ts new file mode 100644 index 0000000..3b34939 --- /dev/null +++ b/packages/plugins/src/types.ts @@ -0,0 +1,80 @@ +/** + * Shared types for Experience SDK plugins + * These types are re-exported by core for user convenience + */ + +/** + * Experience content - varies by type + */ +export type ExperienceContent = BannerContent | ModalContent | TooltipContent; + +/** + * Banner content configuration + */ +export interface BannerContent { + title?: string; + message: string; +} + +/** + * Modal content configuration + */ +export interface ModalContent { + title: string; + message: string; + confirmText?: string; + cancelText?: string; +} + +/** + * Tooltip content configuration + */ +export interface TooltipContent { + message: string; + position?: 'top' | 'bottom' | 'left' | 'right'; +} + +/** + * Experience definition + */ +export interface Experience { + id: string; + type: 'banner' | 'modal' | 'tooltip'; + targeting: Record; + content: ExperienceContent; + frequency?: { + max: number; + per: 'session' | 'day' | 'week'; + }; +} + +/** + * Decision output from evaluation + */ +export interface Decision { + show: boolean; + experienceId?: string; + reasons: string[]; + trace: TraceStep[]; + context: Record; + metadata: DecisionMetadata; +} + +/** + * Trace step for decision path + */ +export interface TraceStep { + step: string; + matched: boolean; + rule?: string; + value?: any; +} + +/** + * Decision metadata + */ +export interface DecisionMetadata { + evaluatedAt: number; + experienceCount: number; + evaluationTimeMs: number; +} diff --git a/packages/plugins/tsconfig.json b/packages/plugins/tsconfig.json index 8a34969..8b411f1 100644 --- a/packages/plugins/tsconfig.json +++ b/packages/plugins/tsconfig.json @@ -6,4 +6,4 @@ }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts"] -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d75c0f0..ac13a23 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,6 +54,12 @@ importers: '@lytics/sdk-kit': specifier: ^0.1.1 version: 0.1.1(typescript@5.9.3) + '@lytics/sdk-kit-plugins': + specifier: ^0.1.2 + version: 0.1.2(@lytics/sdk-kit@0.1.1(typescript@5.9.3)) + '@prosdevlab/experience-sdk-plugins': + specifier: workspace:* + version: link:../plugins devDependencies: '@types/node': specifier: ^24.0.0 @@ -74,11 +80,8 @@ importers: specifier: ^0.1.1 version: 0.1.1(typescript@5.9.3) '@lytics/sdk-kit-plugins': - specifier: ^0.1.0 + specifier: ^0.1.2 version: 0.1.2(@lytics/sdk-kit@0.1.1(typescript@5.9.3)) - '@prosdevlab/experience-sdk': - specifier: workspace:* - version: link:../core devDependencies: tsup: specifier: ^8.5.1 diff --git a/specs/phase-0-foundation/contracts/types.ts b/specs/phase-0-foundation/contracts/types.ts index 433dcef..4be24a3 100644 --- a/specs/phase-0-foundation/contracts/types.ts +++ b/specs/phase-0-foundation/contracts/types.ts @@ -1,6 +1,7 @@ +// biome-ignore lint/suspicious/noExplicitAny: intentional use of any in public API /** * Type Contracts for Experience SDK - * + * * These types define the public API surface and should remain stable. * Breaking changes to these types require a major version bump. */ @@ -84,16 +85,16 @@ export interface UserContext { export interface Decision { show: boolean; experienceId?: string; - reasons: string[]; // Human-readable: ["✅ URL matches", ...] - trace: TraceStep[]; // Machine-readable trace - context: Context; // Input context used + reasons: string[]; // Human-readable: ["✅ URL matches", ...] + trace: TraceStep[]; // Machine-readable trace + context: Context; // Input context used metadata: DecisionMetadata; } export interface TraceStep { - step: string; // e.g., "evaluate-url-rule" + step: string; // e.g., "evaluate-url-rule" timestamp: number; - duration: number; // milliseconds + duration: number; // milliseconds input?: any; output?: any; passed: boolean; @@ -123,4 +124,3 @@ export interface RuntimeState { decisions: Decision[]; config: ExperienceConfig; } - diff --git a/tsconfig.json b/tsconfig.json index 3d26e09..b543e23 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,4 +22,4 @@ }, "exclude": ["node_modules"], "files": [] -} \ No newline at end of file +} diff --git a/turbo.json b/turbo.json index f648966..2fafba6 100644 --- a/turbo.json +++ b/turbo.json @@ -26,4 +26,4 @@ }, "typecheck": {} } -} \ No newline at end of file +} diff --git a/vitest.config.ts b/vitest.config.ts index d0e82e4..5b09a19 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,5 +1,5 @@ +import { resolve } from 'node:path'; import { defineConfig } from 'vitest/config'; -import { resolve } from 'path'; export default defineConfig({ test: { @@ -18,4 +18,4 @@ export default defineConfig({ '@prosdevlab/experience-sdk-plugins': resolve(__dirname, 'packages/plugins/src'), }, }, -}); \ No newline at end of file +});