@lssm/example.learning-patterns 0.0.0-canary-20251213172311

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.turbo/turbo-build.log +39 -0
  2. package/CHANGELOG.md +7 -0
  3. package/README.md +4 -0
  4. package/dist/docs/index.js +1 -0
  5. package/dist/docs/learning-patterns.docblock.js +12 -0
  6. package/dist/events.js +1 -0
  7. package/dist/example.js +1 -0
  8. package/dist/index.js +1 -0
  9. package/dist/libs/contracts/src/docs/PUBLISHING.docblock.js +76 -0
  10. package/dist/libs/contracts/src/docs/accessibility_wcag_compliance_specs.docblock.js +350 -0
  11. package/dist/libs/contracts/src/docs/index.js +1 -0
  12. package/dist/libs/contracts/src/docs/presentations.js +1 -0
  13. package/dist/libs/contracts/src/docs/registry.js +1 -0
  14. package/dist/libs/contracts/src/docs/tech/PHASE_1_QUICKSTART.docblock.js +383 -0
  15. package/dist/libs/contracts/src/docs/tech/PHASE_2_AI_NATIVE_OPERATIONS.docblock.js +68 -0
  16. package/dist/libs/contracts/src/docs/tech/PHASE_3_AUTO_EVOLUTION.docblock.js +140 -0
  17. package/dist/libs/contracts/src/docs/tech/PHASE_4_PERSONALIZATION_ENGINE.docblock.js +86 -0
  18. package/dist/libs/contracts/src/docs/tech/PHASE_5_ZERO_TOUCH_OPERATIONS.docblock.js +1 -0
  19. package/dist/libs/contracts/src/docs/tech/contracts/openapi-export.docblock.js +38 -0
  20. package/dist/libs/contracts/src/docs/tech/lifecycle-stage-system.docblock.js +213 -0
  21. package/dist/libs/contracts/src/docs/tech/mcp-endpoints.docblock.js +1 -0
  22. package/dist/libs/contracts/src/docs/tech/presentation-runtime.docblock.js +1 -0
  23. package/dist/libs/contracts/src/docs/tech/schema/README.docblock.js +262 -0
  24. package/dist/libs/contracts/src/docs/tech/telemetry-ingest.docblock.js +122 -0
  25. package/dist/libs/contracts/src/docs/tech/templates/runtime.docblock.js +1 -0
  26. package/dist/libs/contracts/src/docs/tech/vscode-extension.docblock.js +68 -0
  27. package/dist/libs/contracts/src/docs/tech/workflows/overview.docblock.js +1 -0
  28. package/dist/tracks/ambient-coach.js +1 -0
  29. package/dist/tracks/drills.js +1 -0
  30. package/dist/tracks/index.js +1 -0
  31. package/dist/tracks/quests.js +1 -0
  32. package/example.ts +1 -0
  33. package/package.json +56 -0
  34. package/src/docs/index.ts +3 -0
  35. package/src/docs/learning-patterns.docblock.ts +30 -0
  36. package/src/events.ts +17 -0
  37. package/src/example.ts +25 -0
  38. package/src/index.ts +12 -0
  39. package/src/learning-patterns.test.ts +221 -0
  40. package/src/tracks/ambient-coach.ts +44 -0
  41. package/src/tracks/drills.ts +50 -0
  42. package/src/tracks/index.ts +5 -0
  43. package/src/tracks/quests.ts +40 -0
  44. package/tsconfig.json +11 -0
  45. package/tsconfig.tsbuildinfo +1 -0
  46. package/tsdown.config.js +9 -0
@@ -0,0 +1,68 @@
1
+ import{registerDocBlocks as e}from"../registry.js";e([{id:`docs.tech.vscode.extension`,title:`ContractSpec VS Code Extension`,summary:`VS Code extension for spec-first development with validation, scaffolding, and MCP integration.`,kind:`reference`,visibility:`public`,route:`/docs/tech/vscode/extension`,tags:[`vscode`,`extension`,`tooling`,`dx`],body:`# ContractSpec VS Code Extension
2
+
3
+ The ContractSpec VS Code extension provides spec-first development tooling directly in your editor.
4
+
5
+ ## Features
6
+
7
+ - **Real-time Validation**: Get instant feedback on spec errors and warnings as you save files
8
+ - **Build/Scaffold**: Generate handler and component skeletons from specs (no AI required)
9
+ - **Spec Explorer**: List and navigate all specs in your workspace
10
+ - **Dependency Analysis**: Visualize spec dependencies and detect cycles
11
+ - **MCP Integration**: Search ContractSpec documentation via Model Context Protocol
12
+ - **Snippets**: Code snippets for common ContractSpec patterns
13
+
14
+ ## Commands
15
+
16
+ | Command | Description |
17
+ |---------|-------------|
18
+ | \`ContractSpec: Validate Current Spec\` | Validate the currently open spec file |
19
+ | \`ContractSpec: Validate All Specs\` | Validate all spec files in the workspace |
20
+ | \`ContractSpec: Build/Scaffold\` | Generate handler/component from the current spec |
21
+ | \`ContractSpec: List All Specs\` | Show all specs in the workspace |
22
+ | \`ContractSpec: Analyze Dependencies\` | Analyze and visualize spec dependencies |
23
+ | \`ContractSpec: Search Docs (MCP)\` | Search documentation via MCP |
24
+
25
+ ## Configuration
26
+
27
+ | Setting | Description | Default |
28
+ |---------|-------------|---------|
29
+ | \`contractspec.api.baseUrl\` | Base URL for ContractSpec API (enables MCP + remote telemetry) | \`""\` |
30
+ | \`contractspec.telemetry.posthogHost\` | PostHog host URL for direct telemetry | \`"https://eu.posthog.com"\` |
31
+ | \`contractspec.telemetry.posthogProjectKey\` | PostHog project key for direct telemetry | \`""\` |
32
+ | \`contractspec.validation.onSave\` | Run validation on save | \`true\` |
33
+ | \`contractspec.validation.onOpen\` | Run validation on open | \`true\` |
34
+
35
+ ## Architecture
36
+
37
+ The extension uses:
38
+ - \`@lssm/module.contractspec-workspace\` for pure analysis + templates
39
+ - \`@lssm/bundle.contractspec-workspace\` for workspace services + adapters
40
+
41
+ This allows the extension to work without requiring the CLI to be installed.
42
+
43
+ ## Telemetry
44
+
45
+ The extension uses a hybrid telemetry approach:
46
+ 1. If \`contractspec.api.baseUrl\` is configured → send to API \`/api/telemetry/ingest\`
47
+ 2. Otherwise → send directly to PostHog (if project key configured)
48
+
49
+ Telemetry respects VS Code's telemetry settings. No file paths, source code, or PII is collected.
50
+ `},{id:`docs.tech.vscode.snippets`,title:`ContractSpec Snippets`,summary:`Code snippets for common ContractSpec patterns in VS Code.`,kind:`reference`,visibility:`public`,route:`/docs/tech/vscode/snippets`,tags:[`vscode`,`snippets`,`dx`],body:`# ContractSpec Snippets
51
+
52
+ The VS Code extension includes snippets for common ContractSpec patterns.
53
+
54
+ ## Available Snippets
55
+
56
+ | Prefix | Description |
57
+ |--------|-------------|
58
+ | \`contractspec-command\` | Create a new command (write operation) |
59
+ | \`contractspec-query\` | Create a new query (read-only operation) |
60
+ | \`contractspec-event\` | Create a new event |
61
+ | \`contractspec-docblock\` | Create a new DocBlock |
62
+ | \`contractspec-telemetry\` | Create a new TelemetrySpec |
63
+ | \`contractspec-presentation\` | Create a new Presentation |
64
+
65
+ ## Usage
66
+
67
+ Type the prefix in a TypeScript file and press Tab to expand the snippet. Tab through the placeholders to fill in your values.
68
+ `}]);
@@ -0,0 +1 @@
1
+ import{registerDocBlocks as e}from"../../registry.js";e([{id:`docs.tech.workflows.overview`,title:`WorkflowSpec Overview`,summary:"WorkflowSpec provides a declarative, versioned format for long-running flows that mix automation and human review. Specs stay inside `@lssm/lib.contracts` (`src/workflow/spec.ts`) so the same definition powers runtime execution, documentation, and future generation.",kind:`reference`,visibility:`public`,route:`/docs/tech/workflows/overview`,tags:[`tech`,`workflows`,`overview`],body:"# WorkflowSpec Overview\n\n## Purpose\n\nWorkflowSpec provides a declarative, versioned format for long-running flows that mix automation and human review. Specs stay inside `@lssm/lib.contracts` (`src/workflow/spec.ts`) so the same definition powers runtime execution, documentation, and future generation.\n\n## Core Types\n\n- `WorkflowMeta`: ownership metadata (`title`, `domain`, `owners`, `tags`, `stability`) plus `name` and `version`.\n- `WorkflowDefinition`:\n - `entryStepId?`: optional explicit entry point (defaults to first step).\n - `steps[]`: ordered list of `Step` descriptors.\n - `transitions[]`: directed edges between steps with optional expressions.\n - `sla?`: aggregated timing hints for the overall flow or per-step budgets.\n - `compensation?`: fallback operations executed when a workflow is rolled back or fails.\n- `Step`:\n - `type`: `human`, `automation`, or `decision`.\n - `action`: references either a `ContractSpec` (`operation`) or `FormSpec` (`form`).\n - Optional `guard`, `timeoutMs`, and retry policy (`maxAttempts`, `backoff`, `delayMs`, `maxDelayMs?`).\n - `requiredIntegrations?`: integration slot ids that must be bound before the step may execute.\n - `requiredCapabilities?`: `CapabilityRef[]` that must be enabled in the resolved app config.\n- `Transition`: `from` → `to` with optional `condition` string (simple data expressions).\n\n## Registry & Validation\n\n- `WorkflowRegistry` (`src/workflow/spec.ts`) stores specs by key `<name>.v<version>` and exposes `register`, `list`, and `get`.\n- `validateWorkflowSpec()` (`src/workflow/validation.ts`) checks:\n - Duplicate step IDs.\n - Unknown `from`/`to` transitions.\n - Empty guards/conditions.\n - Reachability from the entry step.\n - Cycles in the graph.\n - Operation/Form references against provided registries.\n- `assertWorkflowSpecValid()` wraps validation and throws `WorkflowValidationError` when errors remain.\n\n## Runtime\n\n- `WorkflowRunner` (`src/workflow/runner.ts`) executes workflows and coordinates steps.\n - `start(name, version?, initialData?)` returns a `workflowId`.\n - `executeStep(workflowId, input?)` runs the current step (automation or human).\n - `getState(workflowId)` retrieves the latest state snapshot.\n - `cancel(workflowId)` marks the workflow as cancelled.\n - `preFlightCheck(name, version?, resolvedConfig?)` evaluates integration/capability requirements before the workflow starts.\n - Throws `WorkflowPreFlightError` if required integration slots are unbound or required capabilities are disabled.\n- `StateStore` (`src/workflow/state.ts`) abstracts persistence. V1 ships with:\n - `InMemoryStateStore` (`src/workflow/adapters/memory-store.ts`) for tests/dev.\n - Placeholder factories for file/database adapters (`adapters/file-adapter.ts`, `adapters/db-adapter.ts`).\n- Guard evaluation: expression guards run through `evaluateExpression()` (`src/workflow/expression.ts`); custom policy guards can be provided via `guardEvaluator`.\n- Events: the runner emits `workflow.started`, `workflow.step_completed`, `workflow.step_failed`, and `workflow.cancelled` through the optional `eventEmitter`.\n- React bindings (`@lssm/lib.presentation-runtime-react`):\n - `useWorkflow` hook (polls state, exposes `executeStep`, `cancel`, `refresh`).\n - `WorkflowStepper` progress indicator using design-system Stepper.\n - `WorkflowStepRenderer` helper to render human/automation/decision steps with sensible fallbacks.\n\n## Authoring Checklist\n\n1. Reuse existing operations/forms; create new specs when missing.\n2. Prefer explicit `entryStepId` for clarity (especially with decision branches).\n3. Give automation steps an `operation` and human steps a `form` (warnings surface otherwise).\n4. Use short, meaningful step IDs (`submit`, `review`, `finalize`) to simplify analytics.\n5. Keep guard expressions deterministic; complex policy logic should move to PolicySpec (Phase 2).\n\n## Testing\n\n- Add unit tests for new workflows via `assertWorkflowSpecValid`.\n- Use the new Vitest suites (`validation.test.ts`, `expression.test.ts`, `runner.test.ts`) as examples.\n- CLI support will arrive in Phase 1 PR 3 (`contractspec create --type workflow`).\n\n## Tooling\n\n- `contractspec create --type workflow` scaffolds a WorkflowSpec with interactive prompts.\n- `contractspec build <spec.workflow.ts>` generates a runner scaffold (`.runner.ts`) wired to `WorkflowRunner` and the in-memory store.\n- `contractspec validate` understands `.workflow.ts` files and checks core structure (meta, steps, transitions).\n\n## Next Steps (Non-MVP)\n\n- Persistence adapters (database/file) for workflow state (Phase 2).\n- React bindings (`useWorkflow`, `WorkflowStepper`) and presentation-runtime integration (PR 3).\n- Policy engine integration (`guard.type === 'policy'` validated against PolicySpec).\n- Telemetry hooks for step execution metrics.\n\n"}]);
@@ -0,0 +1 @@
1
+ import{LEARNING_EVENTS as e}from"../events.js";const t={id:`learning_patterns_ambient_coach_basics`,name:`Ambient Coach Basics`,description:`Contextual tips triggered by behavior events.`,targetUserSegment:`learner`,targetRole:`individual`,totalXp:30,steps:[{id:`tip_shown`,title:`See a contextual tip`,order:1,completion:{kind:`event`,eventName:e.COACH_TIP_SHOWN},xpReward:10},{id:`tip_acknowledged`,title:`Acknowledge a tip`,order:2,completion:{kind:`event`,eventName:e.COACH_TIP_ACKNOWLEDGED},xpReward:10},{id:`tip_action_taken`,title:`Take an action from a tip`,order:3,completion:{kind:`event`,eventName:e.COACH_TIP_ACTION_TAKEN},xpReward:10}]},n=[t];export{t as ambientCoachTrack,n as ambientCoachTracks};
@@ -0,0 +1 @@
1
+ import{LEARNING_EVENTS as e}from"../events.js";const t={id:`learning_patterns_drills_basics`,name:`Drills Basics`,description:`Short drill sessions with an SRS-style mastery step.`,targetUserSegment:`learner`,targetRole:`individual`,totalXp:50,steps:[{id:`complete_first_session`,title:`Complete your first session`,order:1,completion:{kind:`event`,eventName:e.DRILL_SESSION_COMPLETED},xpReward:10},{id:`hit_accuracy_threshold`,title:`Hit high accuracy 3 times`,order:2,completion:{kind:`count`,eventName:e.DRILL_SESSION_COMPLETED,atLeast:3,payloadFilter:{accuracyBucket:`high`}},xpReward:20},{id:`master_cards`,title:`Master 5 cards`,order:3,completion:{kind:`srs_mastery`,eventName:e.DRILL_CARD_MASTERED,minimumMastery:.8,requiredCount:5,skillIdField:`skillId`,masteryField:`mastery`},xpReward:20}]},n=[t];export{n as drillTracks,t as drillsTrack};
@@ -0,0 +1 @@
1
+ import{drillTracks as e,drillsTrack as t}from"./drills.js";import{ambientCoachTrack as n,ambientCoachTracks as r}from"./ambient-coach.js";import{questTrack as i,questTracks as a}from"./quests.js";export{n as ambientCoachTrack,r as ambientCoachTracks,e as drillTracks,t as drillsTrack,i as questTrack,a as questTracks};
@@ -0,0 +1 @@
1
+ import{LEARNING_EVENTS as e}from"../events.js";const t={id:`learning_patterns_quest_7day`,name:`Quest (7-day)`,description:`Time-bounded quest with day unlocks.`,targetUserSegment:`learner`,targetRole:`individual`,totalXp:70,steps:[{id:`day1_start`,title:`Start the quest`,order:1,completion:{kind:`event`,eventName:e.QUEST_STARTED},xpReward:10},{id:`day1_complete`,title:`Complete day 1 step`,order:2,completion:{kind:`event`,eventName:e.QUEST_STEP_COMPLETED},availability:{unlockOnDay:1,dueWithinHours:48},xpReward:10},{id:`day2_complete`,title:`Complete day 2 step`,order:3,completion:{kind:`event`,eventName:e.QUEST_STEP_COMPLETED},availability:{unlockOnDay:2,dueWithinHours:48},xpReward:10}]},n=[t];export{t as questTrack,n as questTracks};
package/example.ts ADDED
@@ -0,0 +1 @@
1
+ export { default } from './src/example';
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@lssm/example.learning-patterns",
3
+ "version": "0.0.0-canary-20251213172311",
4
+ "description": "Example: drills + ambient coach + quests learning patterns, powered by Learning Journey (event-driven, deterministic).",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": "./src/index.ts",
11
+ "./docs": "./src/docs/index.ts",
12
+ "./docs/learning-patterns.docblock": "./src/docs/learning-patterns.docblock.ts",
13
+ "./events": "./src/events.ts",
14
+ "./example": "./src/example.ts",
15
+ "./tracks": "./src/tracks/index.ts",
16
+ "./tracks/ambient-coach": "./src/tracks/ambient-coach.ts",
17
+ "./tracks/drills": "./src/tracks/drills.ts",
18
+ "./tracks/quests": "./src/tracks/quests.ts",
19
+ "./*": "./*"
20
+ },
21
+ "scripts": {
22
+ "build": "bun build:bundle && bun build:types",
23
+ "build:bundle": "tsdown",
24
+ "build:types": "tsc --noEmit",
25
+ "dev": "bun build:bundle --watch",
26
+ "clean": "rimraf dist .turbo",
27
+ "lint": "bun lint:fix",
28
+ "lint:fix": "eslint src --fix",
29
+ "lint:check": "eslint src",
30
+ "test": "bun test"
31
+ },
32
+ "dependencies": {
33
+ "@lssm/module.learning-journey": "workspace:*"
34
+ },
35
+ "devDependencies": {
36
+ "@lssm/tool.tsdown": "workspace:*",
37
+ "@lssm/tool.typescript": "workspace:*",
38
+ "tsdown": "^0.17.0",
39
+ "typescript": "^5.9.3"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public",
43
+ "exports": {
44
+ ".": "./dist/index.js",
45
+ "./docs": "./dist/docs/index.js",
46
+ "./docs/learning-patterns.docblock": "./dist/docs/learning-patterns.docblock.js",
47
+ "./events": "./dist/events.js",
48
+ "./example": "./dist/example.js",
49
+ "./tracks": "./dist/tracks/index.js",
50
+ "./tracks/ambient-coach": "./dist/tracks/ambient-coach.js",
51
+ "./tracks/drills": "./dist/tracks/drills.js",
52
+ "./tracks/quests": "./dist/tracks/quests.js",
53
+ "./*": "./*"
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,3 @@
1
+ import './learning-patterns.docblock';
2
+
3
+
@@ -0,0 +1,30 @@
1
+ import type { DocBlock } from '@lssm/lib.contracts/docs';
2
+ import { registerDocBlocks } from '@lssm/lib.contracts/docs';
3
+
4
+ const docBlocks: DocBlock[] = [
5
+ {
6
+ id: 'docs.examples.learning-patterns.goal',
7
+ title: 'Learning Patterns — Goal',
8
+ summary: 'Domain-agnostic drills, ambient coaching, and quests built on Learning Journey.',
9
+ kind: 'goal',
10
+ visibility: 'public',
11
+ route: '/docs/examples/learning-patterns/goal',
12
+ tags: ['learning', 'drills', 'quests', 'coaching'],
13
+ body: `## Why it matters
14
+ - Demonstrates multiple learning archetypes without vertical coupling.\n- Progress is event-driven (no client-side hacks).\n- SRS logic is deterministic and testable.`,
15
+ },
16
+ {
17
+ id: 'docs.examples.learning-patterns.reference',
18
+ title: 'Learning Patterns — Reference',
19
+ summary: 'Track specs and event names exported by the learning patterns example.',
20
+ kind: 'reference',
21
+ visibility: 'public',
22
+ route: '/docs/examples/learning-patterns',
23
+ tags: ['learning', 'reference'],
24
+ body: `## Tracks\n- Drills + SRS\n- Ambient Coach\n- Quests\n\n## Events\n- drill.*\n- coach.*\n- quest.*`,
25
+ },
26
+ ];
27
+
28
+ registerDocBlocks(docBlocks);
29
+
30
+
package/src/events.ts ADDED
@@ -0,0 +1,17 @@
1
+ export const LEARNING_EVENTS = {
2
+ DRILL_CARD_ANSWERED: 'drill.card.answered',
3
+ DRILL_SESSION_COMPLETED: 'drill.session.completed',
4
+ DRILL_CARD_MASTERED: 'drill.card.mastered',
5
+
6
+ COACH_TIP_TRIGGERED: 'coach.tip.triggered',
7
+ COACH_TIP_SHOWN: 'coach.tip.shown',
8
+ COACH_TIP_ACKNOWLEDGED: 'coach.tip.acknowledged',
9
+ COACH_TIP_ACTION_TAKEN: 'coach.tip.actionTaken',
10
+
11
+ QUEST_STARTED: 'quest.started',
12
+ QUEST_STEP_COMPLETED: 'quest.step.completed',
13
+ } as const;
14
+
15
+ export type LearningEventName = (typeof LEARNING_EVENTS)[keyof typeof LEARNING_EVENTS];
16
+
17
+
package/src/example.ts ADDED
@@ -0,0 +1,25 @@
1
+ const example = {
2
+ id: 'learning-patterns',
3
+ title: 'Learning Patterns',
4
+ summary: 'Domain-agnostic learning archetypes implemented as Learning Journey tracks.',
5
+ tags: ['learning', 'journey', 'patterns'],
6
+ kind: 'library',
7
+ visibility: 'public',
8
+ docs: {
9
+ rootDocId: 'docs.examples.learning-patterns',
10
+ },
11
+ entrypoints: {
12
+ packageName: '@lssm/example.learning-patterns',
13
+ docs: './docs',
14
+ },
15
+ surfaces: {
16
+ templates: true,
17
+ sandbox: { enabled: true, modes: ['markdown', 'specs'] },
18
+ studio: { enabled: true, installable: true },
19
+ mcp: { enabled: true },
20
+ },
21
+ } as const;
22
+
23
+ export default example;
24
+
25
+
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Learning Patterns Example
3
+ *
4
+ * Domain-agnostic learning archetypes implemented as Learning Journey tracks.
5
+ */
6
+ export * from './events';
7
+ export * from './tracks';
8
+ export { default as example } from './example';
9
+
10
+ import './docs';
11
+
12
+
@@ -0,0 +1,221 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import type {
4
+ LearningJourneyTrackSpec,
5
+ StepAvailabilitySpec,
6
+ StepCompletionConditionSpec,
7
+ } from '@lssm/module.learning-journey/track-spec';
8
+ import { SRSEngine } from '@lssm/module.learning-journey/engines/srs';
9
+ import { StreakEngine } from '@lssm/module.learning-journey/engines/streak';
10
+ import { XPEngine } from '@lssm/module.learning-journey/engines/xp';
11
+
12
+ import { ambientCoachTrack } from './tracks/ambient-coach';
13
+ import { drillsTrack } from './tracks/drills';
14
+ import { questTrack } from './tracks/quests';
15
+ import { LEARNING_EVENTS } from './events';
16
+
17
+ interface LearningEvent {
18
+ name: string;
19
+ payload?: Record<string, unknown>;
20
+ occurredAt?: Date;
21
+ }
22
+
23
+ interface StepState {
24
+ id: string;
25
+ status: 'PENDING' | 'COMPLETED';
26
+ occurrences: number;
27
+ masteryCount: number;
28
+ availableAt?: Date;
29
+ dueAt?: Date;
30
+ }
31
+
32
+ const matchesFilter = (
33
+ filter: Record<string, unknown> | undefined,
34
+ payload: Record<string, unknown> | undefined
35
+ ): boolean => {
36
+ if (!filter) return true;
37
+ if (!payload) return false;
38
+ return Object.entries(filter).every(([k, v]) => payload[k] === v);
39
+ };
40
+
41
+ const getAvailability = (
42
+ availability: StepAvailabilitySpec | undefined,
43
+ startedAt: Date | undefined
44
+ ): { availableAt?: Date; dueAt?: Date } => {
45
+ if (!availability || !startedAt) return {};
46
+ const baseTime = startedAt.getTime();
47
+ let unlockTime = baseTime;
48
+ if (availability.unlockOnDay !== undefined) {
49
+ unlockTime = baseTime + (availability.unlockOnDay - 1) * 24 * 60 * 60 * 1000;
50
+ }
51
+ if (availability.unlockAfterHours !== undefined) {
52
+ unlockTime = baseTime + availability.unlockAfterHours * 60 * 60 * 1000;
53
+ }
54
+ const availableAt = new Date(unlockTime);
55
+ const dueAt =
56
+ availability.dueWithinHours !== undefined
57
+ ? new Date(availableAt.getTime() + availability.dueWithinHours * 60 * 60 * 1000)
58
+ : undefined;
59
+ return { availableAt, dueAt };
60
+ };
61
+
62
+ const matchesCondition = (
63
+ condition: StepCompletionConditionSpec,
64
+ event: LearningEvent,
65
+ step: StepState
66
+ ): { matched: boolean; occurrences?: number; masteryCount?: number } => {
67
+ if ((condition.kind ?? 'event') === 'event') {
68
+ if (condition.eventName !== event.name) return { matched: false };
69
+ if (!matchesFilter(condition.payloadFilter, event.payload)) return { matched: false };
70
+ return { matched: true };
71
+ }
72
+ if (condition.kind === 'count') {
73
+ if (condition.eventName !== event.name) return { matched: false };
74
+ if (!matchesFilter(condition.payloadFilter, event.payload)) return { matched: false };
75
+ const occurrences = step.occurrences + 1;
76
+ return { matched: occurrences >= condition.atLeast, occurrences };
77
+ }
78
+ if (condition.kind === 'srs_mastery') {
79
+ if (condition.eventName !== event.name) return { matched: false };
80
+ if (!matchesFilter(condition.payloadFilter, event.payload)) return { matched: false };
81
+ const masteryKey = condition.masteryField ?? 'mastery';
82
+ const masteryValue = event.payload?.[masteryKey];
83
+ if (typeof masteryValue !== 'number') return { matched: false };
84
+ if (masteryValue < condition.minimumMastery) return { matched: false };
85
+ const masteryCount = step.masteryCount + 1;
86
+ const required = condition.requiredCount ?? 1;
87
+ return { matched: masteryCount >= required, masteryCount };
88
+ }
89
+ if (condition.kind === 'time_window') {
90
+ // For this example suite, we treat time_window as a direct match on eventName
91
+ if (condition.eventName !== event.name) return { matched: false };
92
+ return { matched: true };
93
+ }
94
+ return { matched: false };
95
+ };
96
+
97
+ function initProgress(track: LearningJourneyTrackSpec): StepState[] {
98
+ return track.steps.map((s) => ({
99
+ id: s.id,
100
+ status: 'PENDING',
101
+ occurrences: 0,
102
+ masteryCount: 0,
103
+ }));
104
+ }
105
+
106
+ function applyEvents(track: LearningJourneyTrackSpec, events: LearningEvent[]): StepState[] {
107
+ const steps = initProgress(track);
108
+ let startedAt: Date | undefined;
109
+ for (const event of events) {
110
+ const eventTime = event.occurredAt ?? new Date();
111
+ if (!startedAt) startedAt = eventTime;
112
+ for (let index = 0; index < track.steps.length; index++) {
113
+ const spec = track.steps[index];
114
+ const state = steps[index];
115
+ if (!spec || !state) continue;
116
+ if (state.status === 'COMPLETED') continue;
117
+ const { availableAt, dueAt } = getAvailability(spec.availability, startedAt);
118
+ state.availableAt = availableAt;
119
+ state.dueAt = dueAt;
120
+ if (availableAt && eventTime < availableAt) continue;
121
+ if (dueAt && eventTime > dueAt) continue;
122
+ const res = matchesCondition(spec.completion, event, state);
123
+ if (res.occurrences !== undefined) state.occurrences = res.occurrences;
124
+ if (res.masteryCount !== undefined) state.masteryCount = res.masteryCount;
125
+ if (res.matched) state.status = 'COMPLETED';
126
+ }
127
+ }
128
+ return steps;
129
+ }
130
+
131
+ describe('@lssm/example.learning-patterns tracks', () => {
132
+ it('drills track progresses via session count + mastery', () => {
133
+ const events: LearningEvent[] = [
134
+ { name: LEARNING_EVENTS.DRILL_SESSION_COMPLETED },
135
+ { name: LEARNING_EVENTS.DRILL_SESSION_COMPLETED, payload: { accuracyBucket: 'high' } },
136
+ { name: LEARNING_EVENTS.DRILL_SESSION_COMPLETED, payload: { accuracyBucket: 'high' } },
137
+ { name: LEARNING_EVENTS.DRILL_SESSION_COMPLETED, payload: { accuracyBucket: 'high' } },
138
+ ...Array.from({ length: 5 }).map(() => ({
139
+ name: LEARNING_EVENTS.DRILL_CARD_MASTERED,
140
+ payload: { skillId: 's1', mastery: 0.9 },
141
+ })),
142
+ ];
143
+ const progress = applyEvents(drillsTrack, events);
144
+ expect(progress.every((s) => s.status === 'COMPLETED')).toBeTrue();
145
+ });
146
+
147
+ it('ambient coach track progresses via shown -> acknowledged -> actionTaken', () => {
148
+ const progress = applyEvents(ambientCoachTrack, [
149
+ { name: LEARNING_EVENTS.COACH_TIP_SHOWN },
150
+ { name: LEARNING_EVENTS.COACH_TIP_ACKNOWLEDGED },
151
+ { name: LEARNING_EVENTS.COACH_TIP_ACTION_TAKEN },
152
+ ]);
153
+ expect(progress.every((s) => s.status === 'COMPLETED')).toBeTrue();
154
+ });
155
+
156
+ it('quest track respects unlockOnDay availability', () => {
157
+ const start = new Date('2026-01-01T10:00:00.000Z');
158
+ const day1 = new Date('2026-01-01T12:00:00.000Z');
159
+ const day2 = new Date('2026-01-02T12:00:00.000Z');
160
+
161
+ // Attempt to complete steps on day1 (only day1 step should unlock)
162
+ const p1 = applyEvents(questTrack, [
163
+ { name: LEARNING_EVENTS.QUEST_STARTED, occurredAt: start },
164
+ { name: LEARNING_EVENTS.QUEST_STEP_COMPLETED, occurredAt: day1 },
165
+ ]);
166
+ expect(p1[0]?.status).toBe('COMPLETED');
167
+ expect(p1[1]?.status).toBe('COMPLETED');
168
+ expect(p1[2]?.status).toBe('PENDING'); // day2 step not yet available
169
+
170
+ // Now complete on day2
171
+ const p2 = applyEvents(questTrack, [
172
+ { name: LEARNING_EVENTS.QUEST_STARTED, occurredAt: start },
173
+ { name: LEARNING_EVENTS.QUEST_STEP_COMPLETED, occurredAt: day2 },
174
+ ]);
175
+ expect(p2[2]?.status).toBe('COMPLETED');
176
+ });
177
+ });
178
+
179
+ describe('@lssm/example.learning-patterns XP + streak + SRS determinism', () => {
180
+ it('XP engine produces deterministic results for streak bonus inputs', () => {
181
+ const xp = new XPEngine();
182
+ const r1 = xp.calculate({ activity: 'lesson_complete', score: 90, attemptNumber: 1, currentStreak: 7 });
183
+ const r2 = xp.calculate({ activity: 'lesson_complete', score: 90, attemptNumber: 1, currentStreak: 7 });
184
+ expect(r1.totalXp).toBe(r2.totalXp);
185
+ expect(r1.totalXp).toBeGreaterThan(0);
186
+ });
187
+
188
+ it('streak engine increments on consecutive days deterministically', () => {
189
+ const streak = new StreakEngine({ timezone: 'UTC' });
190
+ const initial = {
191
+ currentStreak: 0,
192
+ longestStreak: 0,
193
+ lastActivityAt: null,
194
+ lastActivityDate: null,
195
+ freezesRemaining: 0,
196
+ freezeUsedAt: null,
197
+ };
198
+ const day1 = streak.update(initial, new Date('2026-01-01T10:00:00.000Z')).state;
199
+ const day2 = streak.update(day1, new Date('2026-01-02T10:00:00.000Z')).state;
200
+ expect(day2.currentStreak).toBe(2);
201
+ });
202
+
203
+ it('SRS engine nextReviewAt is deterministic for a fixed now + rating', () => {
204
+ const srs = new SRSEngine();
205
+ const now = new Date('2026-01-01T00:00:00.000Z');
206
+ const state = {
207
+ interval: 0,
208
+ easeFactor: 2.5,
209
+ repetitions: 0,
210
+ learningStep: 0,
211
+ isGraduated: false,
212
+ isRelearning: false,
213
+ lapses: 0,
214
+ };
215
+ const result = srs.calculateNextReview(state, 'GOOD', now);
216
+ // default learningSteps are minutes; first GOOD advances to next step (10 minutes)
217
+ expect(result.nextReviewAt.toISOString()).toBe('2026-01-01T00:10:00.000Z');
218
+ });
219
+ });
220
+
221
+
@@ -0,0 +1,44 @@
1
+ import type { LearningJourneyTrackSpec } from '@lssm/module.learning-journey/track-spec';
2
+ import { LEARNING_EVENTS } from '../events';
3
+
4
+ export const ambientCoachTrack: LearningJourneyTrackSpec = {
5
+ id: 'learning_patterns_ambient_coach_basics',
6
+ name: 'Ambient Coach Basics',
7
+ description: 'Contextual tips triggered by behavior events.',
8
+ targetUserSegment: 'learner',
9
+ targetRole: 'individual',
10
+ totalXp: 30,
11
+ steps: [
12
+ {
13
+ id: 'tip_shown',
14
+ title: 'See a contextual tip',
15
+ order: 1,
16
+ completion: { kind: 'event', eventName: LEARNING_EVENTS.COACH_TIP_SHOWN },
17
+ xpReward: 10,
18
+ },
19
+ {
20
+ id: 'tip_acknowledged',
21
+ title: 'Acknowledge a tip',
22
+ order: 2,
23
+ completion: {
24
+ kind: 'event',
25
+ eventName: LEARNING_EVENTS.COACH_TIP_ACKNOWLEDGED,
26
+ },
27
+ xpReward: 10,
28
+ },
29
+ {
30
+ id: 'tip_action_taken',
31
+ title: 'Take an action from a tip',
32
+ order: 3,
33
+ completion: {
34
+ kind: 'event',
35
+ eventName: LEARNING_EVENTS.COACH_TIP_ACTION_TAKEN,
36
+ },
37
+ xpReward: 10,
38
+ },
39
+ ],
40
+ };
41
+
42
+ export const ambientCoachTracks: LearningJourneyTrackSpec[] = [ambientCoachTrack];
43
+
44
+
@@ -0,0 +1,50 @@
1
+ import type { LearningJourneyTrackSpec } from '@lssm/module.learning-journey/track-spec';
2
+ import { LEARNING_EVENTS } from '../events';
3
+
4
+ export const drillsTrack: LearningJourneyTrackSpec = {
5
+ id: 'learning_patterns_drills_basics',
6
+ name: 'Drills Basics',
7
+ description: 'Short drill sessions with an SRS-style mastery step.',
8
+ targetUserSegment: 'learner',
9
+ targetRole: 'individual',
10
+ totalXp: 50,
11
+ steps: [
12
+ {
13
+ id: 'complete_first_session',
14
+ title: 'Complete your first session',
15
+ order: 1,
16
+ completion: { kind: 'event', eventName: LEARNING_EVENTS.DRILL_SESSION_COMPLETED },
17
+ xpReward: 10,
18
+ },
19
+ {
20
+ id: 'hit_accuracy_threshold',
21
+ title: 'Hit high accuracy 3 times',
22
+ order: 2,
23
+ completion: {
24
+ kind: 'count',
25
+ eventName: LEARNING_EVENTS.DRILL_SESSION_COMPLETED,
26
+ atLeast: 3,
27
+ payloadFilter: { accuracyBucket: 'high' },
28
+ },
29
+ xpReward: 20,
30
+ },
31
+ {
32
+ id: 'master_cards',
33
+ title: 'Master 5 cards',
34
+ order: 3,
35
+ completion: {
36
+ kind: 'srs_mastery',
37
+ eventName: LEARNING_EVENTS.DRILL_CARD_MASTERED,
38
+ minimumMastery: 0.8,
39
+ requiredCount: 5,
40
+ skillIdField: 'skillId',
41
+ masteryField: 'mastery',
42
+ },
43
+ xpReward: 20,
44
+ },
45
+ ],
46
+ };
47
+
48
+ export const drillTracks: LearningJourneyTrackSpec[] = [drillsTrack];
49
+
50
+
@@ -0,0 +1,5 @@
1
+ export * from './drills';
2
+ export * from './ambient-coach';
3
+ export * from './quests';
4
+
5
+
@@ -0,0 +1,40 @@
1
+ import type { LearningJourneyTrackSpec } from '@lssm/module.learning-journey/track-spec';
2
+ import { LEARNING_EVENTS } from '../events';
3
+
4
+ export const questTrack: LearningJourneyTrackSpec = {
5
+ id: 'learning_patterns_quest_7day',
6
+ name: 'Quest (7-day)',
7
+ description: 'Time-bounded quest with day unlocks.',
8
+ targetUserSegment: 'learner',
9
+ targetRole: 'individual',
10
+ totalXp: 70,
11
+ steps: [
12
+ {
13
+ id: 'day1_start',
14
+ title: 'Start the quest',
15
+ order: 1,
16
+ completion: { kind: 'event', eventName: LEARNING_EVENTS.QUEST_STARTED },
17
+ xpReward: 10,
18
+ },
19
+ {
20
+ id: 'day1_complete',
21
+ title: 'Complete day 1 step',
22
+ order: 2,
23
+ completion: { kind: 'event', eventName: LEARNING_EVENTS.QUEST_STEP_COMPLETED },
24
+ availability: { unlockOnDay: 1, dueWithinHours: 48 },
25
+ xpReward: 10,
26
+ },
27
+ {
28
+ id: 'day2_complete',
29
+ title: 'Complete day 2 step',
30
+ order: 3,
31
+ completion: { kind: 'event', eventName: LEARNING_EVENTS.QUEST_STEP_COMPLETED },
32
+ availability: { unlockOnDay: 2, dueWithinHours: 48 },
33
+ xpReward: 10,
34
+ },
35
+ ],
36
+ };
37
+
38
+ export const questTracks: LearningJourneyTrackSpec[] = [questTrack];
39
+
40
+
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "@lssm/tool.typescript/react-library.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["node_modules", "dist"]
9
+ }
10
+
11
+