@contractspec/module.learning-journey 3.7.17 → 4.0.0

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 (65) hide show
  1. package/README.md +3 -2
  2. package/dist/browser/contracts/index.js +1 -1
  3. package/dist/browser/contracts/journey.js +1 -0
  4. package/dist/browser/contracts/operations.js +1 -1
  5. package/dist/browser/docs/index.js +13 -12
  6. package/dist/browser/docs/learning-journey.docblock.js +13 -12
  7. package/dist/browser/engines/index.js +1 -1
  8. package/dist/browser/engines/xp.js +1 -1
  9. package/dist/browser/entities/index.js +1 -1
  10. package/dist/browser/entities/journey.js +1 -0
  11. package/dist/browser/index.js +14 -13
  12. package/dist/browser/learning-journey.feature.js +1 -1
  13. package/dist/browser/runtime/index.js +1 -0
  14. package/dist/browser/runtime/matchers.js +1 -0
  15. package/dist/browser/runtime/progress-state.js +1 -0
  16. package/dist/browser/runtime/snapshot.js +1 -0
  17. package/dist/contracts/index.d.ts +1 -1
  18. package/dist/contracts/index.js +1 -1
  19. package/dist/contracts/{onboarding.d.ts → journey.d.ts} +477 -180
  20. package/dist/contracts/journey.js +2 -0
  21. package/dist/contracts/operations.js +1 -1
  22. package/dist/docs/index.js +13 -12
  23. package/dist/docs/learning-journey.docblock.js +13 -12
  24. package/dist/engines/index.js +1 -1
  25. package/dist/engines/xp.d.ts +1 -1
  26. package/dist/engines/xp.js +1 -1
  27. package/dist/entities/index.d.ts +28 -27
  28. package/dist/entities/index.js +1 -1
  29. package/dist/entities/{onboarding.d.ts → journey.d.ts} +61 -74
  30. package/dist/entities/journey.js +2 -0
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.js +14 -13
  33. package/dist/learning-journey.feature.js +1 -1
  34. package/dist/node/contracts/index.js +1 -1
  35. package/dist/node/contracts/journey.js +1 -0
  36. package/dist/node/contracts/operations.js +1 -1
  37. package/dist/node/docs/index.js +13 -12
  38. package/dist/node/docs/learning-journey.docblock.js +13 -12
  39. package/dist/node/engines/index.js +1 -1
  40. package/dist/node/engines/xp.js +1 -1
  41. package/dist/node/entities/index.js +1 -1
  42. package/dist/node/entities/journey.js +1 -0
  43. package/dist/node/index.js +14 -13
  44. package/dist/node/learning-journey.feature.js +1 -1
  45. package/dist/node/runtime/index.js +1 -0
  46. package/dist/node/runtime/matchers.js +1 -0
  47. package/dist/node/runtime/progress-state.js +1 -0
  48. package/dist/node/runtime/snapshot.js +1 -0
  49. package/dist/runtime/index.d.ts +3 -0
  50. package/dist/runtime/index.js +2 -0
  51. package/dist/runtime/journey-runtime.test.d.ts +1 -0
  52. package/dist/runtime/matchers.d.ts +20 -0
  53. package/dist/runtime/matchers.js +2 -0
  54. package/dist/runtime/progress-state.d.ts +19 -0
  55. package/dist/runtime/progress-state.js +2 -0
  56. package/dist/runtime/snapshot.d.ts +8 -0
  57. package/dist/runtime/snapshot.js +2 -0
  58. package/dist/track-spec.d.ts +118 -87
  59. package/package.json +86 -30
  60. package/dist/browser/contracts/onboarding.js +0 -1
  61. package/dist/browser/entities/onboarding.js +0 -1
  62. package/dist/contracts/onboarding.js +0 -2
  63. package/dist/entities/onboarding.js +0 -2
  64. package/dist/node/contracts/onboarding.js +0 -1
  65. package/dist/node/entities/onboarding.js +0 -1
@@ -0,0 +1 @@
1
+ var R={timezone:"UTC",freezesPerMonth:2,maxFreezes:5,gracePeriodHours:4};class f{config;constructor(e={}){this.config={...R,...e}}update(e,r=new Date){let o=this.getDateString(r),n={state:{...e},streakMaintained:!1,streakLost:!1,freezeUsed:!1,newStreak:!1,daysMissed:0};if(!e.lastActivityDate)return n.state.currentStreak=1,n.state.longestStreak=Math.max(1,e.longestStreak),n.state.lastActivityAt=r,n.state.lastActivityDate=o,n.newStreak=!0,n.streakMaintained=!0,n;if(e.lastActivityDate===o)return n.state.lastActivityAt=r,n.streakMaintained=!0,n;let t=this.getDaysBetween(e.lastActivityDate,o);if(t===1)return n.state.currentStreak=e.currentStreak+1,n.state.longestStreak=Math.max(n.state.currentStreak,e.longestStreak),n.state.lastActivityAt=r,n.state.lastActivityDate=o,n.streakMaintained=!0,n;n.daysMissed=t-1;let s=n.daysMissed;if(s<=e.freezesRemaining)return n.state.freezesRemaining=e.freezesRemaining-s,n.state.freezeUsedAt=r,n.state.currentStreak=e.currentStreak+1,n.state.longestStreak=Math.max(n.state.currentStreak,e.longestStreak),n.state.lastActivityAt=r,n.state.lastActivityDate=o,n.freezeUsed=!0,n.streakMaintained=!0,n;return n.streakLost=!0,n.state.currentStreak=1,n.state.lastActivityAt=r,n.state.lastActivityDate=o,n.newStreak=!0,n}checkStatus(e,r=new Date){if(!e.lastActivityDate)return{isActive:!1,willExpireAt:null,canUseFreeze:!1,daysUntilExpiry:0};let o=this.getDateString(r),n=this.getDaysBetween(e.lastActivityDate,o);if(n===0){let s=this.addDays(r,1);return s.setHours(23,59,59,999),{isActive:!0,willExpireAt:s,canUseFreeze:e.freezesRemaining>0,daysUntilExpiry:1}}if(n===1){let s=new Date(r);return s.setHours(23+this.config.gracePeriodHours,59,59,999),{isActive:!0,willExpireAt:s,canUseFreeze:e.freezesRemaining>0,daysUntilExpiry:0}}let t=n-1;return{isActive:t<=e.freezesRemaining,willExpireAt:null,canUseFreeze:t<=e.freezesRemaining,daysUntilExpiry:-t}}useFreeze(e,r=new Date){if(e.freezesRemaining<=0)return null;return{...e,freezesRemaining:e.freezesRemaining-1,freezeUsedAt:r}}awardMonthlyFreezes(e){return{...e,freezesRemaining:Math.min(e.freezesRemaining+this.config.freezesPerMonth,this.config.maxFreezes)}}getInitialState(){return{currentStreak:0,longestStreak:0,lastActivityAt:null,lastActivityDate:null,freezesRemaining:this.config.freezesPerMonth,freezeUsedAt:null}}getMilestones(e){let r=[3,7,14,30,60,90,180,365,500,1000],o=r.filter((t)=>e>=t),n=r.find((t)=>e<t)??null;return{achieved:o,next:n}}getDateString(e){let r=e.getFullYear(),o=String(e.getMonth()+1).padStart(2,"0"),n=String(e.getDate()).padStart(2,"0");return`${r}-${o}-${n}`}getDaysBetween(e,r){let o=new Date(e),t=new Date(r).getTime()-o.getTime();return Math.floor(t/86400000)}addDays(e,r){return new Date(e.getTime()+r*24*60*60*1000)}}var X=new f;import{defineTranslation as V}from"@contractspec/lib.contracts-spec/translations";var B=V({meta:{key:"learning-journey.messages",version:"1.0.0",domain:"learning-journey",description:"XP source labels for the learning-journey module",owners:["platform"],stability:"experimental"},locale:"en",fallback:"en",messages:{"xp.source.base":{value:"Base",description:"XP breakdown label for base XP"},"xp.source.scoreBonus":{value:"Score Bonus",description:"XP breakdown label for score-based bonus"},"xp.source.perfectScore":{value:"Perfect Score",description:"XP breakdown label for perfect score bonus"},"xp.source.firstAttempt":{value:"First Attempt",description:"XP breakdown label for first attempt bonus"},"xp.source.retryPenalty":{value:"Retry Penalty",description:"XP breakdown label for retry penalty"},"xp.source.streakBonus":{value:"Streak Bonus",description:"XP breakdown label for streak bonus"}}});import{defineTranslation as q}from"@contractspec/lib.contracts-spec/translations";var T=q({meta:{key:"learning-journey.messages",version:"1.0.0",domain:"learning-journey",description:"XP source labels (Spanish)",owners:["platform"],stability:"experimental"},locale:"es",fallback:"en",messages:{"xp.source.base":{value:"Base",description:"XP breakdown label for base XP"},"xp.source.scoreBonus":{value:"Bonificación por puntuación",description:"XP breakdown label for score-based bonus"},"xp.source.perfectScore":{value:"Puntuación perfecta",description:"XP breakdown label for perfect score bonus"},"xp.source.firstAttempt":{value:"Primer intento",description:"XP breakdown label for first attempt bonus"},"xp.source.retryPenalty":{value:"Penalización por reintento",description:"XP breakdown label for retry penalty"},"xp.source.streakBonus":{value:"Bonificación por racha",description:"XP breakdown label for streak bonus"}}});import{defineTranslation as W}from"@contractspec/lib.contracts-spec/translations";var w=W({meta:{key:"learning-journey.messages",version:"1.0.0",domain:"learning-journey",description:"XP source labels (French)",owners:["platform"],stability:"experimental"},locale:"fr",fallback:"en",messages:{"xp.source.base":{value:"Base",description:"XP breakdown label for base XP"},"xp.source.scoreBonus":{value:"Bonus de score",description:"XP breakdown label for score-based bonus"},"xp.source.perfectScore":{value:"Score parfait",description:"XP breakdown label for perfect score bonus"},"xp.source.firstAttempt":{value:"Premier essai",description:"XP breakdown label for first attempt bonus"},"xp.source.retryPenalty":{value:"Pénalité de réessai",description:"XP breakdown label for retry penalty"},"xp.source.streakBonus":{value:"Bonus de série",description:"XP breakdown label for streak bonus"}}});import{createI18nFactory as z}from"@contractspec/lib.contracts-spec/translations";var l=z({specKey:"learning-journey.messages",catalogs:[B,w,T]}),D=l.create,ae=l.getDefault,ye=l.resetRegistry;var g={baseValues:{lesson_complete:10,quiz_pass:20,quiz_perfect:50,flashcard_review:1,course_complete:200,module_complete:50,streak_bonus:5,achievement_unlock:0,daily_goal_complete:15,first_lesson:25,journey_step:5,journey_complete:50},scoreThresholds:[{min:90,multiplier:1.5},{min:80,multiplier:1.25},{min:70,multiplier:1},{min:60,multiplier:0.75},{min:0,multiplier:0.5}],streakTiers:[{days:365,bonus:50},{days:180,bonus:30},{days:90,bonus:20},{days:30,bonus:15},{days:14,bonus:10},{days:7,bonus:5},{days:3,bonus:2},{days:1,bonus:0}],perfectScoreMultiplier:1.5,firstAttemptBonus:10,retryPenalty:0.5,speedBonusMultiplier:1.2,speedBonusThreshold:0.8};class J{config;constructor(e={}){this.config={...g,...e,baseValues:{...g.baseValues,...e.baseValues},scoreThresholds:e.scoreThresholds||g.scoreThresholds,streakTiers:e.streakTiers||g.streakTiers}}calculate(e){let r=[],o=e.baseXp??this.config.baseValues[e.activity],n=o;if(r.push({source:"base",amount:o}),e.score!==void 0){let t=this.getScoreMultiplier(e.score);if(t!==1){let s=Math.round(o*(t-1));n+=s,r.push({source:"score_bonus",amount:s,multiplier:t})}if(e.score===100){let s=Math.round(o*(this.config.perfectScoreMultiplier-1));n+=s,r.push({source:"perfect_score",amount:s,multiplier:this.config.perfectScoreMultiplier})}}if(e.attemptNumber===1&&!e.isRetry)n+=this.config.firstAttemptBonus,r.push({source:"first_attempt",amount:this.config.firstAttemptBonus});if(e.isRetry){let t=Math.round(n*(1-this.config.retryPenalty));n-=t,r.push({source:"retry_penalty",amount:-t,multiplier:this.config.retryPenalty})}if(e.currentStreak&&e.currentStreak>0){let t=this.getStreakBonus(e.currentStreak);if(t>0)n+=t,r.push({source:"streak_bonus",amount:t})}if(o>0)n=Math.max(1,n);return{totalXp:Math.round(n),baseXp:o,breakdown:r}}calculateStreakBonus(e){let r=this.getStreakBonus(e);return{totalXp:r,baseXp:r,breakdown:[{source:"streak_bonus",amount:r}]}}getXpForLevel(e){if(e<=1)return 0;return Math.round(100*Math.pow(e-1,1.5))}getLevelFromXp(e){let r=1,o=this.getXpForLevel(r+1);while(e>=o&&r<1000)r++,o=this.getXpForLevel(r+1);let n=this.getXpForLevel(r),t=this.getXpForLevel(r+1);return{level:r,xpInLevel:e-n,xpForNextLevel:t-n}}getScoreMultiplier(e){for(let r of this.config.scoreThresholds)if(e>=r.min)return r.multiplier;return 1}getStreakBonus(e){for(let r of this.config.streakTiers)if(e>=r.days)return r.bonus;return 0}}var Q={base:"xp.source.base",score_bonus:"xp.source.scoreBonus",perfect_score:"xp.source.perfectScore",first_attempt:"xp.source.firstAttempt",retry_penalty:"xp.source.retryPenalty",streak_bonus:"xp.source.streakBonus"};function fe(e,r){let o=D(r),n=Q[e];return n?o.t(n):e}var ge=new J;var A=(e,r)=>{if(!e)return!0;if(!r)return!1;return Object.entries(e).every(([o,n])=>r[o]===n)},h=(e,r)=>{if(e.eventName!==r.name)return!1;if(e.eventVersion!==void 0&&r.version!==void 0&&e.eventVersion!==r.version)return!1;if(e.sourceModule&&r.sourceModule&&e.sourceModule!==r.sourceModule)return!1;return A(e.payloadFilter,r.payload)},E=(e,r,o,n)=>{if(e.kind==="count"){if(!h(e,r))return{matched:!1};let t=o.occurrences+1;return{matched:(e.withinHours===void 0||n!==void 0&&r.occurredAt!==void 0&&(r.occurredAt.getTime()-n.getTime())/3600000<=e.withinHours)&&t>=e.atLeast,occurrences:t}}if(e.kind==="time_window"){if(!h(e,r))return{matched:!1};if(e.availableAfterHours!==void 0&&n!==void 0&&r.occurredAt!==void 0&&(r.occurredAt.getTime()-n.getTime())/3600000<e.availableAfterHours)return{matched:!1};if(e.withinHoursOfStart!==void 0&&n!==void 0&&r.occurredAt!==void 0&&(r.occurredAt.getTime()-n.getTime())/3600000>e.withinHoursOfStart)return{matched:!1};return{matched:!0}}if(e.kind==="mastery"){if(r.name!==e.eventName)return{matched:!1};if(!A(e.payloadFilter,r.payload))return{matched:!1};let t=e.masteryField??"mastery",s=r.payload?.[t];if(typeof s!=="number")return{matched:!1};if(s<e.minimumMastery)return{matched:!1};let u=o.masteryCount+1;return{masteryCount:u,matched:u>=(e.requiredCount??1)}}return{matched:h(e,r)}},k=(e,r)=>{if(!e||!r)return{};let o=r.getTime(),n=o;if(e.unlockOnDay!==void 0)n=o+(e.unlockOnDay-1)*24*60*60*1000;if(e.unlockAfterHours!==void 0)n=o+e.unlockAfterHours*60*60*1000;let t=new Date(n),s=e.dueWithinHours!==void 0?new Date(t.getTime()+e.dueWithinHours*60*60*1000):void 0;return{availableAt:t,dueAt:s}},I=(e,r)=>{let o=r.steps.find((n)=>n.stepId===e.stepId);if(!o)return!1;if(e.kind==="step_completed")return o.status==="COMPLETED"||o.status==="SKIPPED";return o.selectedBranchKey===e.branchKey},b=(e,r,o,n)=>{if(!e.branches?.length)return;if(!r)return e.branches.find((t)=>t.when===void 0);for(let t of e.branches){if(!t.when)continue;if(E(t.when,r,o,n).matched)return t}return e.branches.find((t)=>t.when===void 0)};var Z=(e)=>e.map((r)=>({...r})),$=(e,r)=>{let o=new Map;for(let n of e.steps){let t=r.steps.find((u)=>u.stepId===n.id),s=n.branches?.find((u)=>u.key===t?.selectedBranchKey);if(!s?.blockStepIds?.length)continue;for(let u of s.blockStepIds)o.set(u,{blockedByBranchKey:s.key,blockedByStepId:n.id})}return o},y=(e,r,o={})=>{let n=o.now??new Date,t={...r,badges:[...r.badges],eventLog:[...r.eventLog],steps:Z(r.steps),streak:{...r.streak}},s=$(e,t);for(let u of e.steps){let c=t.steps.find((S)=>S.stepId===u.id);if(!c)continue;let{availableAt:i,dueAt:d}=k(u.availability,t.startedAt);if(c.availableAt=i,c.dueAt=d,c.status==="COMPLETED"||c.status==="SKIPPED")continue;let a=s.get(u.id);if(a){c.blockedAt??=n,c.blockedByBranchKey=a.blockedByBranchKey,c.blockedByStepId=a.blockedByStepId,c.status="BLOCKED";continue}let m=u.prerequisites??[],p=u.prerequisiteMode==="any"?m.some((S)=>I(S,t)):m.every((S)=>I(S,t)),j=(m.length===0?!0:p)&&(!i||n.getTime()>=i.getTime());if(d&&n.getTime()>d.getTime()){c.missedAt??=n,c.status="MISSED";continue}c.status=j?"AVAILABLE":"LOCKED"}return t},P=(e)=>{let r=e.steps.filter((o)=>o.status!=="BLOCKED");return r.length>0&&r.every((o)=>o.status==="COMPLETED"||o.status==="SKIPPED")},x=(e,r,o={})=>{let n=y(e,r,o),t=n.steps.filter((p)=>p.status!=="BLOCKED"),s=n.steps.filter((p)=>p.status==="COMPLETED"),u=n.steps.filter((p)=>p.status==="AVAILABLE").map((p)=>p.stepId),c=n.steps.filter((p)=>p.status==="BLOCKED").map((p)=>p.stepId),i=n.steps.filter((p)=>p.status==="MISSED").map((p)=>p.stepId),d=t.filter((p)=>p.status==="COMPLETED"||p.status==="SKIPPED").length,a=t.length>0?Math.round(d/t.length*100):0,m=u[0]??null;return{activeStepCount:t.length,availableStepIds:u,badges:[...n.badges],blockedStepIds:c,completedAt:n.completedAt,completedStepCount:d,completedStepIds:s.map((p)=>p.stepId),currentStepId:m,isCompleted:P(n),lastActivityAt:n.lastActivityAt,learnerId:n.learnerId,missedStepIds:i,nextStepId:m,progressPercent:a,startedAt:n.startedAt,steps:n.steps,streakDays:n.streak.currentStreak,totalSteps:e.steps.length,trackId:n.trackId,xpEarned:n.xpEarned}};var C=new f,L=new J,K=(e)=>({...e,badges:[...e.badges],eventLog:[...e.eventLog],steps:e.steps.map((r)=>({...r})),streak:{...e.streak}}),N=(e)=>e.reward?.xp??e.xpReward??0,M=(e,r)=>e.steps.find((o)=>o.stepId===r),v=(e,r)=>{if(r&&!e.badges.includes(r))e.badges.push(r)},O=(e,r,o,n)=>{if(!P(r)||r.completionRewardApplied)return r;let t=e.completionRewards?.xp??0;if(t>0)r.xpEarned+=n.calculate({activity:"journey_complete",baseXp:t,currentStreak:r.streak.currentStreak}).totalXp;return v(r,e.completionRewards?.badgeKey),r.completedAt=o,r.completionRewardApplied=!0,r},H=(e,r,o,n,t,s,u,c)=>{if(n.status==="COMPLETED")return;let i=N(o);if(i>0)r.xpEarned+=c.calculate({activity:"journey_step",baseXp:i,currentStreak:r.streak.currentStreak}).totalXp;n.completedAt=t,n.eventPayload=s?.payload,n.manual=u,n.status="COMPLETED",n.triggeringEvent=s?.name??"journey.step.manual",n.xpEarned=i;let d=b(o,s,n,r.startedAt);if(!d)return;if(n.selectedBranchKey=d.key,d.reward?.xp)r.xpEarned+=d.reward.xp;v(r,d.reward?.badgeKey)},Y=(e,r={})=>y(e,{badges:[],completionRewardApplied:!1,eventLog:[],learnerId:r.learnerId,startedAt:r.now,steps:e.steps.map((o)=>({masteryCount:0,occurrences:0,status:"LOCKED",stepId:o.id,xpEarned:0})),streak:C.getInitialState(),trackId:e.id,xpEarned:0},{now:r.now}),G=(e,r,o,n={})=>{let t=n.streakEngine??C,s=n.xpEngine??L,u=o.occurredAt??new Date,c=y(e,K(r),{now:u});c.eventLog.push({...o,occurredAt:u,trackId:e.id}),c.lastActivityAt=u,c.startedAt??=u,c.streak=t.update(c.streak,u).state;for(let i of e.steps){let d=M(c,i.id);if(!d||d.status!=="AVAILABLE")continue;let a=E(i.completion,o,d,c.startedAt);if(a.occurrences!==void 0)d.occurrences=a.occurrences;if(a.masteryCount!==void 0)d.masteryCount=a.masteryCount;if(!a.matched)continue;H(e,c,i,d,u,o,!1,s)}return O(e,y(e,c,{now:u}),u,s)},U=(e,r,o,n={})=>{let t=n.streakEngine??C,s=n.xpEngine??L,u=n.now??new Date,c=y(e,K(r),{now:u}),i=e.steps.find((a)=>a.id===o),d=M(c,o);if(!i||!d||d.status!=="AVAILABLE")return c;return c.eventLog.push({name:"journey.step.manual",occurredAt:u,trackId:e.id}),c.lastActivityAt=u,c.startedAt??=u,c.streak=t.update(c.streak,u).state,H(e,c,i,d,u,{name:"journey.step.manual",occurredAt:u},!0,s),O(e,y(e,c,{now:u}),u,s)},_=(e,r,o={})=>x(e,r,o);export{y as synchronizeJourneyProgressState,b as resolveJourneyBranch,k as resolveJourneyAvailability,G as recordJourneyEvent,_ as projectJourneyProgress,A as matchesPayloadFilter,E as matchesJourneyCondition,h as matchesBaseJourneyEvent,P as isJourneyComplete,Y as createJourneyProgressState,U as completeJourneyStep,x as buildJourneyProgressSnapshot};
@@ -0,0 +1 @@
1
+ var d=(e,r)=>{if(!e)return!0;if(!r)return!1;return Object.entries(e).every(([u,n])=>r[u]===n)},o=(e,r)=>{if(e.eventName!==r.name)return!1;if(e.eventVersion!==void 0&&r.version!==void 0&&e.eventVersion!==r.version)return!1;if(e.sourceModule&&r.sourceModule&&e.sourceModule!==r.sourceModule)return!1;return d(e.payloadFilter,r.payload)},f=(e,r,u,n)=>{if(e.kind==="count"){if(!o(e,r))return{matched:!1};let t=u.occurrences+1;return{matched:(e.withinHours===void 0||n!==void 0&&r.occurredAt!==void 0&&(r.occurredAt.getTime()-n.getTime())/3600000<=e.withinHours)&&t>=e.atLeast,occurrences:t}}if(e.kind==="time_window"){if(!o(e,r))return{matched:!1};if(e.availableAfterHours!==void 0&&n!==void 0&&r.occurredAt!==void 0&&(r.occurredAt.getTime()-n.getTime())/3600000<e.availableAfterHours)return{matched:!1};if(e.withinHoursOfStart!==void 0&&n!==void 0&&r.occurredAt!==void 0&&(r.occurredAt.getTime()-n.getTime())/3600000>e.withinHoursOfStart)return{matched:!1};return{matched:!0}}if(e.kind==="mastery"){if(r.name!==e.eventName)return{matched:!1};if(!d(e.payloadFilter,r.payload))return{matched:!1};let t=e.masteryField??"mastery",s=r.payload?.[t];if(typeof s!=="number")return{matched:!1};if(s<e.minimumMastery)return{matched:!1};let a=u.masteryCount+1;return{masteryCount:a,matched:a>=(e.requiredCount??1)}}return{matched:o(e,r)}},i=(e,r)=>{if(!e||!r)return{};let u=r.getTime(),n=u;if(e.unlockOnDay!==void 0)n=u+(e.unlockOnDay-1)*24*60*60*1000;if(e.unlockAfterHours!==void 0)n=u+e.unlockAfterHours*60*60*1000;let t=new Date(n),s=e.dueWithinHours!==void 0?new Date(t.getTime()+e.dueWithinHours*60*60*1000):void 0;return{availableAt:t,dueAt:s}},c=(e,r)=>{let u=r.steps.find((n)=>n.stepId===e.stepId);if(!u)return!1;if(e.kind==="step_completed")return u.status==="COMPLETED"||u.status==="SKIPPED";return u.selectedBranchKey===e.branchKey},m=(e,r,u,n)=>{if(!e.branches?.length)return;if(!r)return e.branches.find((t)=>t.when===void 0);for(let t of e.branches){if(!t.when)continue;if(f(t.when,r,u,n).matched)return t}return e.branches.find((t)=>t.when===void 0)};export{m as resolveJourneyBranch,i as resolveJourneyAvailability,d as matchesPayloadFilter,f as matchesJourneyCondition,o as matchesBaseJourneyEvent,c as isJourneyPrerequisiteSatisfied};
@@ -0,0 +1 @@
1
+ var V={timezone:"UTC",freezesPerMonth:2,maxFreezes:5,gracePeriodHours:4};class m{config;constructor(e={}){this.config={...V,...e}}update(e,r=new Date){let o=this.getDateString(r),n={state:{...e},streakMaintained:!1,streakLost:!1,freezeUsed:!1,newStreak:!1,daysMissed:0};if(!e.lastActivityDate)return n.state.currentStreak=1,n.state.longestStreak=Math.max(1,e.longestStreak),n.state.lastActivityAt=r,n.state.lastActivityDate=o,n.newStreak=!0,n.streakMaintained=!0,n;if(e.lastActivityDate===o)return n.state.lastActivityAt=r,n.streakMaintained=!0,n;let t=this.getDaysBetween(e.lastActivityDate,o);if(t===1)return n.state.currentStreak=e.currentStreak+1,n.state.longestStreak=Math.max(n.state.currentStreak,e.longestStreak),n.state.lastActivityAt=r,n.state.lastActivityDate=o,n.streakMaintained=!0,n;n.daysMissed=t-1;let s=n.daysMissed;if(s<=e.freezesRemaining)return n.state.freezesRemaining=e.freezesRemaining-s,n.state.freezeUsedAt=r,n.state.currentStreak=e.currentStreak+1,n.state.longestStreak=Math.max(n.state.currentStreak,e.longestStreak),n.state.lastActivityAt=r,n.state.lastActivityDate=o,n.freezeUsed=!0,n.streakMaintained=!0,n;return n.streakLost=!0,n.state.currentStreak=1,n.state.lastActivityAt=r,n.state.lastActivityDate=o,n.newStreak=!0,n}checkStatus(e,r=new Date){if(!e.lastActivityDate)return{isActive:!1,willExpireAt:null,canUseFreeze:!1,daysUntilExpiry:0};let o=this.getDateString(r),n=this.getDaysBetween(e.lastActivityDate,o);if(n===0){let s=this.addDays(r,1);return s.setHours(23,59,59,999),{isActive:!0,willExpireAt:s,canUseFreeze:e.freezesRemaining>0,daysUntilExpiry:1}}if(n===1){let s=new Date(r);return s.setHours(23+this.config.gracePeriodHours,59,59,999),{isActive:!0,willExpireAt:s,canUseFreeze:e.freezesRemaining>0,daysUntilExpiry:0}}let t=n-1;return{isActive:t<=e.freezesRemaining,willExpireAt:null,canUseFreeze:t<=e.freezesRemaining,daysUntilExpiry:-t}}useFreeze(e,r=new Date){if(e.freezesRemaining<=0)return null;return{...e,freezesRemaining:e.freezesRemaining-1,freezeUsedAt:r}}awardMonthlyFreezes(e){return{...e,freezesRemaining:Math.min(e.freezesRemaining+this.config.freezesPerMonth,this.config.maxFreezes)}}getInitialState(){return{currentStreak:0,longestStreak:0,lastActivityAt:null,lastActivityDate:null,freezesRemaining:this.config.freezesPerMonth,freezeUsedAt:null}}getMilestones(e){let r=[3,7,14,30,60,90,180,365,500,1000],o=r.filter((t)=>e>=t),n=r.find((t)=>e<t)??null;return{achieved:o,next:n}}getDateString(e){let r=e.getFullYear(),o=String(e.getMonth()+1).padStart(2,"0"),n=String(e.getDate()).padStart(2,"0");return`${r}-${o}-${n}`}getDaysBetween(e,r){let o=new Date(e),t=new Date(r).getTime()-o.getTime();return Math.floor(t/86400000)}addDays(e,r){return new Date(e.getTime()+r*24*60*60*1000)}}var G=new m;import{defineTranslation as j}from"@contractspec/lib.contracts-spec/translations";var I=j({meta:{key:"learning-journey.messages",version:"1.0.0",domain:"learning-journey",description:"XP source labels for the learning-journey module",owners:["platform"],stability:"experimental"},locale:"en",fallback:"en",messages:{"xp.source.base":{value:"Base",description:"XP breakdown label for base XP"},"xp.source.scoreBonus":{value:"Score Bonus",description:"XP breakdown label for score-based bonus"},"xp.source.perfectScore":{value:"Perfect Score",description:"XP breakdown label for perfect score bonus"},"xp.source.firstAttempt":{value:"First Attempt",description:"XP breakdown label for first attempt bonus"},"xp.source.retryPenalty":{value:"Retry Penalty",description:"XP breakdown label for retry penalty"},"xp.source.streakBonus":{value:"Streak Bonus",description:"XP breakdown label for streak bonus"}}});import{defineTranslation as q}from"@contractspec/lib.contracts-spec/translations";var b=q({meta:{key:"learning-journey.messages",version:"1.0.0",domain:"learning-journey",description:"XP source labels (Spanish)",owners:["platform"],stability:"experimental"},locale:"es",fallback:"en",messages:{"xp.source.base":{value:"Base",description:"XP breakdown label for base XP"},"xp.source.scoreBonus":{value:"Bonificación por puntuación",description:"XP breakdown label for score-based bonus"},"xp.source.perfectScore":{value:"Puntuación perfecta",description:"XP breakdown label for perfect score bonus"},"xp.source.firstAttempt":{value:"Primer intento",description:"XP breakdown label for first attempt bonus"},"xp.source.retryPenalty":{value:"Penalización por reintento",description:"XP breakdown label for retry penalty"},"xp.source.streakBonus":{value:"Bonificación por racha",description:"XP breakdown label for streak bonus"}}});import{defineTranslation as W}from"@contractspec/lib.contracts-spec/translations";var x=W({meta:{key:"learning-journey.messages",version:"1.0.0",domain:"learning-journey",description:"XP source labels (French)",owners:["platform"],stability:"experimental"},locale:"fr",fallback:"en",messages:{"xp.source.base":{value:"Base",description:"XP breakdown label for base XP"},"xp.source.scoreBonus":{value:"Bonus de score",description:"XP breakdown label for score-based bonus"},"xp.source.perfectScore":{value:"Score parfait",description:"XP breakdown label for perfect score bonus"},"xp.source.firstAttempt":{value:"Premier essai",description:"XP breakdown label for first attempt bonus"},"xp.source.retryPenalty":{value:"Pénalité de réessai",description:"XP breakdown label for retry penalty"},"xp.source.streakBonus":{value:"Bonus de série",description:"XP breakdown label for streak bonus"}}});import{createI18nFactory as z}from"@contractspec/lib.contracts-spec/translations";var h=z({specKey:"learning-journey.messages",catalogs:[I,x,b]}),C=h.create,ce=h.getDefault,de=h.resetRegistry;var g={baseValues:{lesson_complete:10,quiz_pass:20,quiz_perfect:50,flashcard_review:1,course_complete:200,module_complete:50,streak_bonus:5,achievement_unlock:0,daily_goal_complete:15,first_lesson:25,journey_step:5,journey_complete:50},scoreThresholds:[{min:90,multiplier:1.5},{min:80,multiplier:1.25},{min:70,multiplier:1},{min:60,multiplier:0.75},{min:0,multiplier:0.5}],streakTiers:[{days:365,bonus:50},{days:180,bonus:30},{days:90,bonus:20},{days:30,bonus:15},{days:14,bonus:10},{days:7,bonus:5},{days:3,bonus:2},{days:1,bonus:0}],perfectScoreMultiplier:1.5,firstAttemptBonus:10,retryPenalty:0.5,speedBonusMultiplier:1.2,speedBonusThreshold:0.8};class J{config;constructor(e={}){this.config={...g,...e,baseValues:{...g.baseValues,...e.baseValues},scoreThresholds:e.scoreThresholds||g.scoreThresholds,streakTiers:e.streakTiers||g.streakTiers}}calculate(e){let r=[],o=e.baseXp??this.config.baseValues[e.activity],n=o;if(r.push({source:"base",amount:o}),e.score!==void 0){let t=this.getScoreMultiplier(e.score);if(t!==1){let s=Math.round(o*(t-1));n+=s,r.push({source:"score_bonus",amount:s,multiplier:t})}if(e.score===100){let s=Math.round(o*(this.config.perfectScoreMultiplier-1));n+=s,r.push({source:"perfect_score",amount:s,multiplier:this.config.perfectScoreMultiplier})}}if(e.attemptNumber===1&&!e.isRetry)n+=this.config.firstAttemptBonus,r.push({source:"first_attempt",amount:this.config.firstAttemptBonus});if(e.isRetry){let t=Math.round(n*(1-this.config.retryPenalty));n-=t,r.push({source:"retry_penalty",amount:-t,multiplier:this.config.retryPenalty})}if(e.currentStreak&&e.currentStreak>0){let t=this.getStreakBonus(e.currentStreak);if(t>0)n+=t,r.push({source:"streak_bonus",amount:t})}if(o>0)n=Math.max(1,n);return{totalXp:Math.round(n),baseXp:o,breakdown:r}}calculateStreakBonus(e){let r=this.getStreakBonus(e);return{totalXp:r,baseXp:r,breakdown:[{source:"streak_bonus",amount:r}]}}getXpForLevel(e){if(e<=1)return 0;return Math.round(100*Math.pow(e-1,1.5))}getLevelFromXp(e){let r=1,o=this.getXpForLevel(r+1);while(e>=o&&r<1000)r++,o=this.getXpForLevel(r+1);let n=this.getXpForLevel(r),t=this.getXpForLevel(r+1);return{level:r,xpInLevel:e-n,xpForNextLevel:t-n}}getScoreMultiplier(e){for(let r of this.config.scoreThresholds)if(e>=r.min)return r.multiplier;return 1}getStreakBonus(e){for(let r of this.config.streakTiers)if(e>=r.days)return r.bonus;return 0}}var Q={base:"xp.source.base",score_bonus:"xp.source.scoreBonus",perfect_score:"xp.source.perfectScore",first_attempt:"xp.source.firstAttempt",retry_penalty:"xp.source.retryPenalty",streak_bonus:"xp.source.streakBonus"};function ae(e,r){let o=C(r),n=Q[e];return n?o.t(n):e}var ye=new J;var T=(e,r)=>{if(!e)return!0;if(!r)return!1;return Object.entries(e).every(([o,n])=>r[o]===n)},E=(e,r)=>{if(e.eventName!==r.name)return!1;if(e.eventVersion!==void 0&&r.version!==void 0&&e.eventVersion!==r.version)return!1;if(e.sourceModule&&r.sourceModule&&e.sourceModule!==r.sourceModule)return!1;return T(e.payloadFilter,r.payload)},P=(e,r,o,n)=>{if(e.kind==="count"){if(!E(e,r))return{matched:!1};let t=o.occurrences+1;return{matched:(e.withinHours===void 0||n!==void 0&&r.occurredAt!==void 0&&(r.occurredAt.getTime()-n.getTime())/3600000<=e.withinHours)&&t>=e.atLeast,occurrences:t}}if(e.kind==="time_window"){if(!E(e,r))return{matched:!1};if(e.availableAfterHours!==void 0&&n!==void 0&&r.occurredAt!==void 0&&(r.occurredAt.getTime()-n.getTime())/3600000<e.availableAfterHours)return{matched:!1};if(e.withinHoursOfStart!==void 0&&n!==void 0&&r.occurredAt!==void 0&&(r.occurredAt.getTime()-n.getTime())/3600000>e.withinHoursOfStart)return{matched:!1};return{matched:!0}}if(e.kind==="mastery"){if(r.name!==e.eventName)return{matched:!1};if(!T(e.payloadFilter,r.payload))return{matched:!1};let t=e.masteryField??"mastery",s=r.payload?.[t];if(typeof s!=="number")return{matched:!1};if(s<e.minimumMastery)return{matched:!1};let u=o.masteryCount+1;return{masteryCount:u,matched:u>=(e.requiredCount??1)}}return{matched:E(e,r)}},B=(e,r)=>{if(!e||!r)return{};let o=r.getTime(),n=o;if(e.unlockOnDay!==void 0)n=o+(e.unlockOnDay-1)*24*60*60*1000;if(e.unlockAfterHours!==void 0)n=o+e.unlockAfterHours*60*60*1000;let t=new Date(n),s=e.dueWithinHours!==void 0?new Date(t.getTime()+e.dueWithinHours*60*60*1000):void 0;return{availableAt:t,dueAt:s}},l=(e,r)=>{let o=r.steps.find((n)=>n.stepId===e.stepId);if(!o)return!1;if(e.kind==="step_completed")return o.status==="COMPLETED"||o.status==="SKIPPED";return o.selectedBranchKey===e.branchKey},w=(e,r,o,n)=>{if(!e.branches?.length)return;if(!r)return e.branches.find((t)=>t.when===void 0);for(let t of e.branches){if(!t.when)continue;if(P(t.when,r,o,n).matched)return t}return e.branches.find((t)=>t.when===void 0)};var Z=(e)=>e.map((r)=>({...r})),$=(e,r)=>{let o=new Map;for(let n of e.steps){let t=r.steps.find((u)=>u.stepId===n.id),s=n.branches?.find((u)=>u.key===t?.selectedBranchKey);if(!s?.blockStepIds?.length)continue;for(let u of s.blockStepIds)o.set(u,{blockedByBranchKey:s.key,blockedByStepId:n.id})}return o},y=(e,r,o={})=>{let n=o.now??new Date,t={...r,badges:[...r.badges],eventLog:[...r.eventLog],steps:Z(r.steps),streak:{...r.streak}},s=$(e,t);for(let u of e.steps){let c=t.steps.find((S)=>S.stepId===u.id);if(!c)continue;let{availableAt:i,dueAt:d}=B(u.availability,t.startedAt);if(c.availableAt=i,c.dueAt=d,c.status==="COMPLETED"||c.status==="SKIPPED")continue;let a=s.get(u.id);if(a){c.blockedAt??=n,c.blockedByBranchKey=a.blockedByBranchKey,c.blockedByStepId=a.blockedByStepId,c.status="BLOCKED";continue}let f=u.prerequisites??[],p=u.prerequisiteMode==="any"?f.some((S)=>l(S,t)):f.every((S)=>l(S,t)),R=(f.length===0?!0:p)&&(!i||n.getTime()>=i.getTime());if(d&&n.getTime()>d.getTime()){c.missedAt??=n,c.status="MISSED";continue}c.status=R?"AVAILABLE":"LOCKED"}return t},A=(e)=>{let r=e.steps.filter((o)=>o.status!=="BLOCKED");return r.length>0&&r.every((o)=>o.status==="COMPLETED"||o.status==="SKIPPED")},D=(e,r,o={})=>{let n=y(e,r,o),t=n.steps.filter((p)=>p.status!=="BLOCKED"),s=n.steps.filter((p)=>p.status==="COMPLETED"),u=n.steps.filter((p)=>p.status==="AVAILABLE").map((p)=>p.stepId),c=n.steps.filter((p)=>p.status==="BLOCKED").map((p)=>p.stepId),i=n.steps.filter((p)=>p.status==="MISSED").map((p)=>p.stepId),d=t.filter((p)=>p.status==="COMPLETED"||p.status==="SKIPPED").length,a=t.length>0?Math.round(d/t.length*100):0,f=u[0]??null;return{activeStepCount:t.length,availableStepIds:u,badges:[...n.badges],blockedStepIds:c,completedAt:n.completedAt,completedStepCount:d,completedStepIds:s.map((p)=>p.stepId),currentStepId:f,isCompleted:A(n),lastActivityAt:n.lastActivityAt,learnerId:n.learnerId,missedStepIds:i,nextStepId:f,progressPercent:a,startedAt:n.startedAt,steps:n.steps,streakDays:n.streak.currentStreak,totalSteps:e.steps.length,trackId:n.trackId,xpEarned:n.xpEarned}};var k=new m,L=new J,K=(e)=>({...e,badges:[...e.badges],eventLog:[...e.eventLog],steps:e.steps.map((r)=>({...r})),streak:{...e.streak}}),N=(e)=>e.reward?.xp??e.xpReward??0,M=(e,r)=>e.steps.find((o)=>o.stepId===r),O=(e,r)=>{if(r&&!e.badges.includes(r))e.badges.push(r)},H=(e,r,o,n)=>{if(!A(r)||r.completionRewardApplied)return r;let t=e.completionRewards?.xp??0;if(t>0)r.xpEarned+=n.calculate({activity:"journey_complete",baseXp:t,currentStreak:r.streak.currentStreak}).totalXp;return O(r,e.completionRewards?.badgeKey),r.completedAt=o,r.completionRewardApplied=!0,r},v=(e,r,o,n,t,s,u,c)=>{if(n.status==="COMPLETED")return;let i=N(o);if(i>0)r.xpEarned+=c.calculate({activity:"journey_step",baseXp:i,currentStreak:r.streak.currentStreak}).totalXp;n.completedAt=t,n.eventPayload=s?.payload,n.manual=u,n.status="COMPLETED",n.triggeringEvent=s?.name??"journey.step.manual",n.xpEarned=i;let d=w(o,s,n,r.startedAt);if(!d)return;if(n.selectedBranchKey=d.key,d.reward?.xp)r.xpEarned+=d.reward.xp;O(r,d.reward?.badgeKey)},le=(e,r={})=>y(e,{badges:[],completionRewardApplied:!1,eventLog:[],learnerId:r.learnerId,startedAt:r.now,steps:e.steps.map((o)=>({masteryCount:0,occurrences:0,status:"LOCKED",stepId:o.id,xpEarned:0})),streak:k.getInitialState(),trackId:e.id,xpEarned:0},{now:r.now}),Ae=(e,r,o,n={})=>{let t=n.streakEngine??k,s=n.xpEngine??L,u=o.occurredAt??new Date,c=y(e,K(r),{now:u});c.eventLog.push({...o,occurredAt:u,trackId:e.id}),c.lastActivityAt=u,c.startedAt??=u,c.streak=t.update(c.streak,u).state;for(let i of e.steps){let d=M(c,i.id);if(!d||d.status!=="AVAILABLE")continue;let a=P(i.completion,o,d,c.startedAt);if(a.occurrences!==void 0)d.occurrences=a.occurrences;if(a.masteryCount!==void 0)d.masteryCount=a.masteryCount;if(!a.matched)continue;v(e,c,i,d,u,o,!1,s)}return H(e,y(e,c,{now:u}),u,s)},ke=(e,r,o,n={})=>{let t=n.streakEngine??k,s=n.xpEngine??L,u=n.now??new Date,c=y(e,K(r),{now:u}),i=e.steps.find((a)=>a.id===o),d=M(c,o);if(!i||!d||d.status!=="AVAILABLE")return c;return c.eventLog.push({name:"journey.step.manual",occurredAt:u,trackId:e.id}),c.lastActivityAt=u,c.startedAt??=u,c.streak=t.update(c.streak,u).state,v(e,c,i,d,u,{name:"journey.step.manual",occurredAt:u},!0,s),H(e,y(e,c,{now:u}),u,s)},Ie=(e,r,o={})=>D(e,r,o);export{Ae as recordJourneyEvent,Ie as projectJourneyProgress,le as createJourneyProgressState,ke as completeJourneyStep};
@@ -0,0 +1 @@
1
+ var S=(e,t)=>{if(!e)return!0;if(!t)return!1;return Object.entries(e).every(([n,r])=>t[n]===r)},y=(e,t)=>{if(e.eventName!==t.name)return!1;if(e.eventVersion!==void 0&&t.version!==void 0&&e.eventVersion!==t.version)return!1;if(e.sourceModule&&t.sourceModule&&e.sourceModule!==t.sourceModule)return!1;return S(e.payloadFilter,t.payload)},J=(e,t,n,r)=>{if(e.kind==="count"){if(!y(e,t))return{matched:!1};let s=n.occurrences+1;return{matched:(e.withinHours===void 0||r!==void 0&&t.occurredAt!==void 0&&(t.occurredAt.getTime()-r.getTime())/3600000<=e.withinHours)&&s>=e.atLeast,occurrences:s}}if(e.kind==="time_window"){if(!y(e,t))return{matched:!1};if(e.availableAfterHours!==void 0&&r!==void 0&&t.occurredAt!==void 0&&(t.occurredAt.getTime()-r.getTime())/3600000<e.availableAfterHours)return{matched:!1};if(e.withinHoursOfStart!==void 0&&r!==void 0&&t.occurredAt!==void 0&&(t.occurredAt.getTime()-r.getTime())/3600000>e.withinHoursOfStart)return{matched:!1};return{matched:!0}}if(e.kind==="mastery"){if(t.name!==e.eventName)return{matched:!1};if(!S(e.payloadFilter,t.payload))return{matched:!1};let s=e.masteryField??"mastery",a=t.payload?.[s];if(typeof a!=="number")return{matched:!1};if(a<e.minimumMastery)return{matched:!1};let u=n.masteryCount+1;return{masteryCount:u,matched:u>=(e.requiredCount??1)}}return{matched:y(e,t)}},h=(e,t)=>{if(!e||!t)return{};let n=t.getTime(),r=n;if(e.unlockOnDay!==void 0)r=n+(e.unlockOnDay-1)*24*60*60*1000;if(e.unlockAfterHours!==void 0)r=n+e.unlockAfterHours*60*60*1000;let s=new Date(r),a=e.dueWithinHours!==void 0?new Date(s.getTime()+e.dueWithinHours*60*60*1000):void 0;return{availableAt:s,dueAt:a}},m=(e,t)=>{let n=t.steps.find((r)=>r.stepId===e.stepId);if(!n)return!1;if(e.kind==="step_completed")return n.status==="COMPLETED"||n.status==="SKIPPED";return n.selectedBranchKey===e.branchKey},E=(e,t,n,r)=>{if(!e.branches?.length)return;if(!t)return e.branches.find((s)=>s.when===void 0);for(let s of e.branches){if(!s.when)continue;if(J(s.when,t,n,r).matched)return s}return e.branches.find((s)=>s.when===void 0)};var b=(e)=>e.map((t)=>({...t})),k=(e,t)=>{let n=new Map;for(let r of e.steps){let s=t.steps.find((u)=>u.stepId===r.id),a=r.branches?.find((u)=>u.key===s?.selectedBranchKey);if(!a?.blockStepIds?.length)continue;for(let u of a.blockStepIds)n.set(u,{blockedByBranchKey:a.key,blockedByStepId:r.id})}return n},P=(e,t,n={})=>{let r=n.now??new Date,s={...t,badges:[...t.badges],eventLog:[...t.eventLog],steps:b(t.steps),streak:{...t.streak}},a=k(e,s);for(let u of e.steps){let c=s.steps.find((f)=>f.stepId===u.id);if(!c)continue;let{availableAt:p,dueAt:d}=h(u.availability,s.startedAt);if(c.availableAt=p,c.dueAt=d,c.status==="COMPLETED"||c.status==="SKIPPED")continue;let l=a.get(u.id);if(l){c.blockedAt??=r,c.blockedByBranchKey=l.blockedByBranchKey,c.blockedByStepId=l.blockedByStepId,c.status="BLOCKED";continue}let i=u.prerequisites??[],o=u.prerequisiteMode==="any"?i.some((f)=>m(f,s)):i.every((f)=>m(f,s)),g=(i.length===0?!0:o)&&(!p||r.getTime()>=p.getTime());if(d&&r.getTime()>d.getTime()){c.missedAt??=r,c.status="MISSED";continue}c.status=g?"AVAILABLE":"LOCKED"}return s},A=(e)=>{let t=e.steps.filter((n)=>n.status!=="BLOCKED");return t.length>0&&t.every((n)=>n.status==="COMPLETED"||n.status==="SKIPPED")},w=(e,t,n={})=>{let r=P(e,t,n),s=r.steps.filter((o)=>o.status!=="BLOCKED"),a=r.steps.filter((o)=>o.status==="COMPLETED"),u=r.steps.filter((o)=>o.status==="AVAILABLE").map((o)=>o.stepId),c=r.steps.filter((o)=>o.status==="BLOCKED").map((o)=>o.stepId),p=r.steps.filter((o)=>o.status==="MISSED").map((o)=>o.stepId),d=s.filter((o)=>o.status==="COMPLETED"||o.status==="SKIPPED").length,l=s.length>0?Math.round(d/s.length*100):0,i=u[0]??null;return{activeStepCount:s.length,availableStepIds:u,badges:[...r.badges],blockedStepIds:c,completedAt:r.completedAt,completedStepCount:d,completedStepIds:a.map((o)=>o.stepId),currentStepId:i,isCompleted:A(r),lastActivityAt:r.lastActivityAt,learnerId:r.learnerId,missedStepIds:p,nextStepId:i,progressPercent:l,startedAt:r.startedAt,steps:r.steps,streakDays:r.streak.currentStreak,totalSteps:e.steps.length,trackId:r.trackId,xpEarned:r.xpEarned}};export{P as synchronizeJourneyProgressState,A as isJourneyComplete,w as buildJourneyProgressSnapshot};
@@ -0,0 +1,3 @@
1
+ export { matchesBaseJourneyEvent, matchesJourneyCondition, matchesPayloadFilter, resolveJourneyAvailability, resolveJourneyBranch, } from './matchers';
2
+ export { completeJourneyStep, createJourneyProgressState, projectJourneyProgress, recordJourneyEvent, } from './progress-state';
3
+ export { buildJourneyProgressSnapshot, isJourneyComplete, synchronizeJourneyProgressState, } from './snapshot';
@@ -0,0 +1,2 @@
1
+ // @bun
2
+ var R={timezone:"UTC",freezesPerMonth:2,maxFreezes:5,gracePeriodHours:4};class f{config;constructor(e={}){this.config={...R,...e}}update(e,r=new Date){let o=this.getDateString(r),n={state:{...e},streakMaintained:!1,streakLost:!1,freezeUsed:!1,newStreak:!1,daysMissed:0};if(!e.lastActivityDate)return n.state.currentStreak=1,n.state.longestStreak=Math.max(1,e.longestStreak),n.state.lastActivityAt=r,n.state.lastActivityDate=o,n.newStreak=!0,n.streakMaintained=!0,n;if(e.lastActivityDate===o)return n.state.lastActivityAt=r,n.streakMaintained=!0,n;let t=this.getDaysBetween(e.lastActivityDate,o);if(t===1)return n.state.currentStreak=e.currentStreak+1,n.state.longestStreak=Math.max(n.state.currentStreak,e.longestStreak),n.state.lastActivityAt=r,n.state.lastActivityDate=o,n.streakMaintained=!0,n;n.daysMissed=t-1;let s=n.daysMissed;if(s<=e.freezesRemaining)return n.state.freezesRemaining=e.freezesRemaining-s,n.state.freezeUsedAt=r,n.state.currentStreak=e.currentStreak+1,n.state.longestStreak=Math.max(n.state.currentStreak,e.longestStreak),n.state.lastActivityAt=r,n.state.lastActivityDate=o,n.freezeUsed=!0,n.streakMaintained=!0,n;return n.streakLost=!0,n.state.currentStreak=1,n.state.lastActivityAt=r,n.state.lastActivityDate=o,n.newStreak=!0,n}checkStatus(e,r=new Date){if(!e.lastActivityDate)return{isActive:!1,willExpireAt:null,canUseFreeze:!1,daysUntilExpiry:0};let o=this.getDateString(r),n=this.getDaysBetween(e.lastActivityDate,o);if(n===0){let s=this.addDays(r,1);return s.setHours(23,59,59,999),{isActive:!0,willExpireAt:s,canUseFreeze:e.freezesRemaining>0,daysUntilExpiry:1}}if(n===1){let s=new Date(r);return s.setHours(23+this.config.gracePeriodHours,59,59,999),{isActive:!0,willExpireAt:s,canUseFreeze:e.freezesRemaining>0,daysUntilExpiry:0}}let t=n-1;return{isActive:t<=e.freezesRemaining,willExpireAt:null,canUseFreeze:t<=e.freezesRemaining,daysUntilExpiry:-t}}useFreeze(e,r=new Date){if(e.freezesRemaining<=0)return null;return{...e,freezesRemaining:e.freezesRemaining-1,freezeUsedAt:r}}awardMonthlyFreezes(e){return{...e,freezesRemaining:Math.min(e.freezesRemaining+this.config.freezesPerMonth,this.config.maxFreezes)}}getInitialState(){return{currentStreak:0,longestStreak:0,lastActivityAt:null,lastActivityDate:null,freezesRemaining:this.config.freezesPerMonth,freezeUsedAt:null}}getMilestones(e){let r=[3,7,14,30,60,90,180,365,500,1000],o=r.filter((t)=>e>=t),n=r.find((t)=>e<t)??null;return{achieved:o,next:n}}getDateString(e){let r=e.getFullYear(),o=String(e.getMonth()+1).padStart(2,"0"),n=String(e.getDate()).padStart(2,"0");return`${r}-${o}-${n}`}getDaysBetween(e,r){let o=new Date(e),t=new Date(r).getTime()-o.getTime();return Math.floor(t/86400000)}addDays(e,r){return new Date(e.getTime()+r*24*60*60*1000)}}var X=new f;import{defineTranslation as V}from"@contractspec/lib.contracts-spec/translations";var B=V({meta:{key:"learning-journey.messages",version:"1.0.0",domain:"learning-journey",description:"XP source labels for the learning-journey module",owners:["platform"],stability:"experimental"},locale:"en",fallback:"en",messages:{"xp.source.base":{value:"Base",description:"XP breakdown label for base XP"},"xp.source.scoreBonus":{value:"Score Bonus",description:"XP breakdown label for score-based bonus"},"xp.source.perfectScore":{value:"Perfect Score",description:"XP breakdown label for perfect score bonus"},"xp.source.firstAttempt":{value:"First Attempt",description:"XP breakdown label for first attempt bonus"},"xp.source.retryPenalty":{value:"Retry Penalty",description:"XP breakdown label for retry penalty"},"xp.source.streakBonus":{value:"Streak Bonus",description:"XP breakdown label for streak bonus"}}});import{defineTranslation as q}from"@contractspec/lib.contracts-spec/translations";var T=q({meta:{key:"learning-journey.messages",version:"1.0.0",domain:"learning-journey",description:"XP source labels (Spanish)",owners:["platform"],stability:"experimental"},locale:"es",fallback:"en",messages:{"xp.source.base":{value:"Base",description:"XP breakdown label for base XP"},"xp.source.scoreBonus":{value:"Bonificaci\xF3n por puntuaci\xF3n",description:"XP breakdown label for score-based bonus"},"xp.source.perfectScore":{value:"Puntuaci\xF3n perfecta",description:"XP breakdown label for perfect score bonus"},"xp.source.firstAttempt":{value:"Primer intento",description:"XP breakdown label for first attempt bonus"},"xp.source.retryPenalty":{value:"Penalizaci\xF3n por reintento",description:"XP breakdown label for retry penalty"},"xp.source.streakBonus":{value:"Bonificaci\xF3n por racha",description:"XP breakdown label for streak bonus"}}});import{defineTranslation as W}from"@contractspec/lib.contracts-spec/translations";var w=W({meta:{key:"learning-journey.messages",version:"1.0.0",domain:"learning-journey",description:"XP source labels (French)",owners:["platform"],stability:"experimental"},locale:"fr",fallback:"en",messages:{"xp.source.base":{value:"Base",description:"XP breakdown label for base XP"},"xp.source.scoreBonus":{value:"Bonus de score",description:"XP breakdown label for score-based bonus"},"xp.source.perfectScore":{value:"Score parfait",description:"XP breakdown label for perfect score bonus"},"xp.source.firstAttempt":{value:"Premier essai",description:"XP breakdown label for first attempt bonus"},"xp.source.retryPenalty":{value:"P\xE9nalit\xE9 de r\xE9essai",description:"XP breakdown label for retry penalty"},"xp.source.streakBonus":{value:"Bonus de s\xE9rie",description:"XP breakdown label for streak bonus"}}});import{createI18nFactory as z}from"@contractspec/lib.contracts-spec/translations";var l=z({specKey:"learning-journey.messages",catalogs:[B,w,T]}),D=l.create,ae=l.getDefault,ye=l.resetRegistry;var g={baseValues:{lesson_complete:10,quiz_pass:20,quiz_perfect:50,flashcard_review:1,course_complete:200,module_complete:50,streak_bonus:5,achievement_unlock:0,daily_goal_complete:15,first_lesson:25,journey_step:5,journey_complete:50},scoreThresholds:[{min:90,multiplier:1.5},{min:80,multiplier:1.25},{min:70,multiplier:1},{min:60,multiplier:0.75},{min:0,multiplier:0.5}],streakTiers:[{days:365,bonus:50},{days:180,bonus:30},{days:90,bonus:20},{days:30,bonus:15},{days:14,bonus:10},{days:7,bonus:5},{days:3,bonus:2},{days:1,bonus:0}],perfectScoreMultiplier:1.5,firstAttemptBonus:10,retryPenalty:0.5,speedBonusMultiplier:1.2,speedBonusThreshold:0.8};class J{config;constructor(e={}){this.config={...g,...e,baseValues:{...g.baseValues,...e.baseValues},scoreThresholds:e.scoreThresholds||g.scoreThresholds,streakTiers:e.streakTiers||g.streakTiers}}calculate(e){let r=[],o=e.baseXp??this.config.baseValues[e.activity],n=o;if(r.push({source:"base",amount:o}),e.score!==void 0){let t=this.getScoreMultiplier(e.score);if(t!==1){let s=Math.round(o*(t-1));n+=s,r.push({source:"score_bonus",amount:s,multiplier:t})}if(e.score===100){let s=Math.round(o*(this.config.perfectScoreMultiplier-1));n+=s,r.push({source:"perfect_score",amount:s,multiplier:this.config.perfectScoreMultiplier})}}if(e.attemptNumber===1&&!e.isRetry)n+=this.config.firstAttemptBonus,r.push({source:"first_attempt",amount:this.config.firstAttemptBonus});if(e.isRetry){let t=Math.round(n*(1-this.config.retryPenalty));n-=t,r.push({source:"retry_penalty",amount:-t,multiplier:this.config.retryPenalty})}if(e.currentStreak&&e.currentStreak>0){let t=this.getStreakBonus(e.currentStreak);if(t>0)n+=t,r.push({source:"streak_bonus",amount:t})}if(o>0)n=Math.max(1,n);return{totalXp:Math.round(n),baseXp:o,breakdown:r}}calculateStreakBonus(e){let r=this.getStreakBonus(e);return{totalXp:r,baseXp:r,breakdown:[{source:"streak_bonus",amount:r}]}}getXpForLevel(e){if(e<=1)return 0;return Math.round(100*Math.pow(e-1,1.5))}getLevelFromXp(e){let r=1,o=this.getXpForLevel(r+1);while(e>=o&&r<1000)r++,o=this.getXpForLevel(r+1);let n=this.getXpForLevel(r),t=this.getXpForLevel(r+1);return{level:r,xpInLevel:e-n,xpForNextLevel:t-n}}getScoreMultiplier(e){for(let r of this.config.scoreThresholds)if(e>=r.min)return r.multiplier;return 1}getStreakBonus(e){for(let r of this.config.streakTiers)if(e>=r.days)return r.bonus;return 0}}var Q={base:"xp.source.base",score_bonus:"xp.source.scoreBonus",perfect_score:"xp.source.perfectScore",first_attempt:"xp.source.firstAttempt",retry_penalty:"xp.source.retryPenalty",streak_bonus:"xp.source.streakBonus"};function fe(e,r){let o=D(r),n=Q[e];return n?o.t(n):e}var ge=new J;var A=(e,r)=>{if(!e)return!0;if(!r)return!1;return Object.entries(e).every(([o,n])=>r[o]===n)},h=(e,r)=>{if(e.eventName!==r.name)return!1;if(e.eventVersion!==void 0&&r.version!==void 0&&e.eventVersion!==r.version)return!1;if(e.sourceModule&&r.sourceModule&&e.sourceModule!==r.sourceModule)return!1;return A(e.payloadFilter,r.payload)},E=(e,r,o,n)=>{if(e.kind==="count"){if(!h(e,r))return{matched:!1};let t=o.occurrences+1;return{matched:(e.withinHours===void 0||n!==void 0&&r.occurredAt!==void 0&&(r.occurredAt.getTime()-n.getTime())/3600000<=e.withinHours)&&t>=e.atLeast,occurrences:t}}if(e.kind==="time_window"){if(!h(e,r))return{matched:!1};if(e.availableAfterHours!==void 0&&n!==void 0&&r.occurredAt!==void 0&&(r.occurredAt.getTime()-n.getTime())/3600000<e.availableAfterHours)return{matched:!1};if(e.withinHoursOfStart!==void 0&&n!==void 0&&r.occurredAt!==void 0&&(r.occurredAt.getTime()-n.getTime())/3600000>e.withinHoursOfStart)return{matched:!1};return{matched:!0}}if(e.kind==="mastery"){if(r.name!==e.eventName)return{matched:!1};if(!A(e.payloadFilter,r.payload))return{matched:!1};let t=e.masteryField??"mastery",s=r.payload?.[t];if(typeof s!=="number")return{matched:!1};if(s<e.minimumMastery)return{matched:!1};let u=o.masteryCount+1;return{masteryCount:u,matched:u>=(e.requiredCount??1)}}return{matched:h(e,r)}},k=(e,r)=>{if(!e||!r)return{};let o=r.getTime(),n=o;if(e.unlockOnDay!==void 0)n=o+(e.unlockOnDay-1)*24*60*60*1000;if(e.unlockAfterHours!==void 0)n=o+e.unlockAfterHours*60*60*1000;let t=new Date(n),s=e.dueWithinHours!==void 0?new Date(t.getTime()+e.dueWithinHours*60*60*1000):void 0;return{availableAt:t,dueAt:s}},I=(e,r)=>{let o=r.steps.find((n)=>n.stepId===e.stepId);if(!o)return!1;if(e.kind==="step_completed")return o.status==="COMPLETED"||o.status==="SKIPPED";return o.selectedBranchKey===e.branchKey},b=(e,r,o,n)=>{if(!e.branches?.length)return;if(!r)return e.branches.find((t)=>t.when===void 0);for(let t of e.branches){if(!t.when)continue;if(E(t.when,r,o,n).matched)return t}return e.branches.find((t)=>t.when===void 0)};var Z=(e)=>e.map((r)=>({...r})),$=(e,r)=>{let o=new Map;for(let n of e.steps){let t=r.steps.find((u)=>u.stepId===n.id),s=n.branches?.find((u)=>u.key===t?.selectedBranchKey);if(!s?.blockStepIds?.length)continue;for(let u of s.blockStepIds)o.set(u,{blockedByBranchKey:s.key,blockedByStepId:n.id})}return o},y=(e,r,o={})=>{let n=o.now??new Date,t={...r,badges:[...r.badges],eventLog:[...r.eventLog],steps:Z(r.steps),streak:{...r.streak}},s=$(e,t);for(let u of e.steps){let c=t.steps.find((S)=>S.stepId===u.id);if(!c)continue;let{availableAt:i,dueAt:d}=k(u.availability,t.startedAt);if(c.availableAt=i,c.dueAt=d,c.status==="COMPLETED"||c.status==="SKIPPED")continue;let a=s.get(u.id);if(a){c.blockedAt??=n,c.blockedByBranchKey=a.blockedByBranchKey,c.blockedByStepId=a.blockedByStepId,c.status="BLOCKED";continue}let m=u.prerequisites??[],p=u.prerequisiteMode==="any"?m.some((S)=>I(S,t)):m.every((S)=>I(S,t)),j=(m.length===0?!0:p)&&(!i||n.getTime()>=i.getTime());if(d&&n.getTime()>d.getTime()){c.missedAt??=n,c.status="MISSED";continue}c.status=j?"AVAILABLE":"LOCKED"}return t},P=(e)=>{let r=e.steps.filter((o)=>o.status!=="BLOCKED");return r.length>0&&r.every((o)=>o.status==="COMPLETED"||o.status==="SKIPPED")},x=(e,r,o={})=>{let n=y(e,r,o),t=n.steps.filter((p)=>p.status!=="BLOCKED"),s=n.steps.filter((p)=>p.status==="COMPLETED"),u=n.steps.filter((p)=>p.status==="AVAILABLE").map((p)=>p.stepId),c=n.steps.filter((p)=>p.status==="BLOCKED").map((p)=>p.stepId),i=n.steps.filter((p)=>p.status==="MISSED").map((p)=>p.stepId),d=t.filter((p)=>p.status==="COMPLETED"||p.status==="SKIPPED").length,a=t.length>0?Math.round(d/t.length*100):0,m=u[0]??null;return{activeStepCount:t.length,availableStepIds:u,badges:[...n.badges],blockedStepIds:c,completedAt:n.completedAt,completedStepCount:d,completedStepIds:s.map((p)=>p.stepId),currentStepId:m,isCompleted:P(n),lastActivityAt:n.lastActivityAt,learnerId:n.learnerId,missedStepIds:i,nextStepId:m,progressPercent:a,startedAt:n.startedAt,steps:n.steps,streakDays:n.streak.currentStreak,totalSteps:e.steps.length,trackId:n.trackId,xpEarned:n.xpEarned}};var C=new f,L=new J,K=(e)=>({...e,badges:[...e.badges],eventLog:[...e.eventLog],steps:e.steps.map((r)=>({...r})),streak:{...e.streak}}),N=(e)=>e.reward?.xp??e.xpReward??0,M=(e,r)=>e.steps.find((o)=>o.stepId===r),v=(e,r)=>{if(r&&!e.badges.includes(r))e.badges.push(r)},O=(e,r,o,n)=>{if(!P(r)||r.completionRewardApplied)return r;let t=e.completionRewards?.xp??0;if(t>0)r.xpEarned+=n.calculate({activity:"journey_complete",baseXp:t,currentStreak:r.streak.currentStreak}).totalXp;return v(r,e.completionRewards?.badgeKey),r.completedAt=o,r.completionRewardApplied=!0,r},H=(e,r,o,n,t,s,u,c)=>{if(n.status==="COMPLETED")return;let i=N(o);if(i>0)r.xpEarned+=c.calculate({activity:"journey_step",baseXp:i,currentStreak:r.streak.currentStreak}).totalXp;n.completedAt=t,n.eventPayload=s?.payload,n.manual=u,n.status="COMPLETED",n.triggeringEvent=s?.name??"journey.step.manual",n.xpEarned=i;let d=b(o,s,n,r.startedAt);if(!d)return;if(n.selectedBranchKey=d.key,d.reward?.xp)r.xpEarned+=d.reward.xp;v(r,d.reward?.badgeKey)},Y=(e,r={})=>y(e,{badges:[],completionRewardApplied:!1,eventLog:[],learnerId:r.learnerId,startedAt:r.now,steps:e.steps.map((o)=>({masteryCount:0,occurrences:0,status:"LOCKED",stepId:o.id,xpEarned:0})),streak:C.getInitialState(),trackId:e.id,xpEarned:0},{now:r.now}),G=(e,r,o,n={})=>{let t=n.streakEngine??C,s=n.xpEngine??L,u=o.occurredAt??new Date,c=y(e,K(r),{now:u});c.eventLog.push({...o,occurredAt:u,trackId:e.id}),c.lastActivityAt=u,c.startedAt??=u,c.streak=t.update(c.streak,u).state;for(let i of e.steps){let d=M(c,i.id);if(!d||d.status!=="AVAILABLE")continue;let a=E(i.completion,o,d,c.startedAt);if(a.occurrences!==void 0)d.occurrences=a.occurrences;if(a.masteryCount!==void 0)d.masteryCount=a.masteryCount;if(!a.matched)continue;H(e,c,i,d,u,o,!1,s)}return O(e,y(e,c,{now:u}),u,s)},U=(e,r,o,n={})=>{let t=n.streakEngine??C,s=n.xpEngine??L,u=n.now??new Date,c=y(e,K(r),{now:u}),i=e.steps.find((a)=>a.id===o),d=M(c,o);if(!i||!d||d.status!=="AVAILABLE")return c;return c.eventLog.push({name:"journey.step.manual",occurredAt:u,trackId:e.id}),c.lastActivityAt=u,c.startedAt??=u,c.streak=t.update(c.streak,u).state,H(e,c,i,d,u,{name:"journey.step.manual",occurredAt:u},!0,s),O(e,y(e,c,{now:u}),u,s)},_=(e,r,o={})=>x(e,r,o);export{y as synchronizeJourneyProgressState,b as resolveJourneyBranch,k as resolveJourneyAvailability,G as recordJourneyEvent,_ as projectJourneyProgress,A as matchesPayloadFilter,E as matchesJourneyCondition,h as matchesBaseJourneyEvent,P as isJourneyComplete,Y as createJourneyProgressState,U as completeJourneyStep,x as buildJourneyProgressSnapshot};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ import type { JourneyAvailabilitySpec, JourneyBranchSpec, JourneyConditionSpec, JourneyEvent, JourneyPrerequisiteSpec, JourneyProgressState, JourneyStepProgressState, JourneyStepSpec } from '../track-spec';
2
+ export interface JourneyConditionMatch {
3
+ masteryCount?: number;
4
+ matched: boolean;
5
+ occurrences?: number;
6
+ }
7
+ export declare const matchesPayloadFilter: (filter: Record<string, unknown> | undefined, payload: Record<string, unknown> | undefined) => boolean;
8
+ export declare const matchesBaseJourneyEvent: (condition: {
9
+ eventName: string;
10
+ eventVersion?: number;
11
+ payloadFilter?: Record<string, unknown>;
12
+ sourceModule?: string;
13
+ }, event: JourneyEvent) => boolean;
14
+ export declare const matchesJourneyCondition: (condition: JourneyConditionSpec, event: JourneyEvent, step: JourneyStepProgressState, trackStartedAt: Date | undefined) => JourneyConditionMatch;
15
+ export declare const resolveJourneyAvailability: (availability: JourneyAvailabilitySpec | undefined, startedAt: Date | undefined) => {
16
+ availableAt?: Date;
17
+ dueAt?: Date;
18
+ };
19
+ export declare const isJourneyPrerequisiteSatisfied: (prerequisite: JourneyPrerequisiteSpec, progress: JourneyProgressState) => boolean;
20
+ export declare const resolveJourneyBranch: (step: JourneyStepSpec, event: JourneyEvent | undefined, progress: JourneyStepProgressState, trackStartedAt: Date | undefined) => JourneyBranchSpec | undefined;
@@ -0,0 +1,2 @@
1
+ // @bun
2
+ var d=(e,r)=>{if(!e)return!0;if(!r)return!1;return Object.entries(e).every(([u,n])=>r[u]===n)},o=(e,r)=>{if(e.eventName!==r.name)return!1;if(e.eventVersion!==void 0&&r.version!==void 0&&e.eventVersion!==r.version)return!1;if(e.sourceModule&&r.sourceModule&&e.sourceModule!==r.sourceModule)return!1;return d(e.payloadFilter,r.payload)},f=(e,r,u,n)=>{if(e.kind==="count"){if(!o(e,r))return{matched:!1};let t=u.occurrences+1;return{matched:(e.withinHours===void 0||n!==void 0&&r.occurredAt!==void 0&&(r.occurredAt.getTime()-n.getTime())/3600000<=e.withinHours)&&t>=e.atLeast,occurrences:t}}if(e.kind==="time_window"){if(!o(e,r))return{matched:!1};if(e.availableAfterHours!==void 0&&n!==void 0&&r.occurredAt!==void 0&&(r.occurredAt.getTime()-n.getTime())/3600000<e.availableAfterHours)return{matched:!1};if(e.withinHoursOfStart!==void 0&&n!==void 0&&r.occurredAt!==void 0&&(r.occurredAt.getTime()-n.getTime())/3600000>e.withinHoursOfStart)return{matched:!1};return{matched:!0}}if(e.kind==="mastery"){if(r.name!==e.eventName)return{matched:!1};if(!d(e.payloadFilter,r.payload))return{matched:!1};let t=e.masteryField??"mastery",s=r.payload?.[t];if(typeof s!=="number")return{matched:!1};if(s<e.minimumMastery)return{matched:!1};let a=u.masteryCount+1;return{masteryCount:a,matched:a>=(e.requiredCount??1)}}return{matched:o(e,r)}},i=(e,r)=>{if(!e||!r)return{};let u=r.getTime(),n=u;if(e.unlockOnDay!==void 0)n=u+(e.unlockOnDay-1)*24*60*60*1000;if(e.unlockAfterHours!==void 0)n=u+e.unlockAfterHours*60*60*1000;let t=new Date(n),s=e.dueWithinHours!==void 0?new Date(t.getTime()+e.dueWithinHours*60*60*1000):void 0;return{availableAt:t,dueAt:s}},c=(e,r)=>{let u=r.steps.find((n)=>n.stepId===e.stepId);if(!u)return!1;if(e.kind==="step_completed")return u.status==="COMPLETED"||u.status==="SKIPPED";return u.selectedBranchKey===e.branchKey},m=(e,r,u,n)=>{if(!e.branches?.length)return;if(!r)return e.branches.find((t)=>t.when===void 0);for(let t of e.branches){if(!t.when)continue;if(f(t.when,r,u,n).matched)return t}return e.branches.find((t)=>t.when===void 0)};export{m as resolveJourneyBranch,i as resolveJourneyAvailability,d as matchesPayloadFilter,f as matchesJourneyCondition,o as matchesBaseJourneyEvent,c as isJourneyPrerequisiteSatisfied};
@@ -0,0 +1,19 @@
1
+ import { StreakEngine } from '../engines/streak';
2
+ import { XPEngine } from '../engines/xp';
3
+ import type { JourneyEvent, JourneyProgressState, JourneyTrackSpec } from '../track-spec';
4
+ export declare const createJourneyProgressState: (track: JourneyTrackSpec, options?: {
5
+ learnerId?: string;
6
+ now?: Date;
7
+ }) => JourneyProgressState;
8
+ export declare const recordJourneyEvent: (track: JourneyTrackSpec, state: JourneyProgressState, event: JourneyEvent, options?: {
9
+ streakEngine?: StreakEngine;
10
+ xpEngine?: XPEngine;
11
+ }) => JourneyProgressState;
12
+ export declare const completeJourneyStep: (track: JourneyTrackSpec, state: JourneyProgressState, stepId: string, options?: {
13
+ now?: Date;
14
+ streakEngine?: StreakEngine;
15
+ xpEngine?: XPEngine;
16
+ }) => JourneyProgressState;
17
+ export declare const projectJourneyProgress: (track: JourneyTrackSpec, state: JourneyProgressState, options?: {
18
+ now?: Date;
19
+ }) => import("..").JourneyProgressSnapshot;
@@ -0,0 +1,2 @@
1
+ // @bun
2
+ var V={timezone:"UTC",freezesPerMonth:2,maxFreezes:5,gracePeriodHours:4};class m{config;constructor(e={}){this.config={...V,...e}}update(e,r=new Date){let o=this.getDateString(r),n={state:{...e},streakMaintained:!1,streakLost:!1,freezeUsed:!1,newStreak:!1,daysMissed:0};if(!e.lastActivityDate)return n.state.currentStreak=1,n.state.longestStreak=Math.max(1,e.longestStreak),n.state.lastActivityAt=r,n.state.lastActivityDate=o,n.newStreak=!0,n.streakMaintained=!0,n;if(e.lastActivityDate===o)return n.state.lastActivityAt=r,n.streakMaintained=!0,n;let t=this.getDaysBetween(e.lastActivityDate,o);if(t===1)return n.state.currentStreak=e.currentStreak+1,n.state.longestStreak=Math.max(n.state.currentStreak,e.longestStreak),n.state.lastActivityAt=r,n.state.lastActivityDate=o,n.streakMaintained=!0,n;n.daysMissed=t-1;let s=n.daysMissed;if(s<=e.freezesRemaining)return n.state.freezesRemaining=e.freezesRemaining-s,n.state.freezeUsedAt=r,n.state.currentStreak=e.currentStreak+1,n.state.longestStreak=Math.max(n.state.currentStreak,e.longestStreak),n.state.lastActivityAt=r,n.state.lastActivityDate=o,n.freezeUsed=!0,n.streakMaintained=!0,n;return n.streakLost=!0,n.state.currentStreak=1,n.state.lastActivityAt=r,n.state.lastActivityDate=o,n.newStreak=!0,n}checkStatus(e,r=new Date){if(!e.lastActivityDate)return{isActive:!1,willExpireAt:null,canUseFreeze:!1,daysUntilExpiry:0};let o=this.getDateString(r),n=this.getDaysBetween(e.lastActivityDate,o);if(n===0){let s=this.addDays(r,1);return s.setHours(23,59,59,999),{isActive:!0,willExpireAt:s,canUseFreeze:e.freezesRemaining>0,daysUntilExpiry:1}}if(n===1){let s=new Date(r);return s.setHours(23+this.config.gracePeriodHours,59,59,999),{isActive:!0,willExpireAt:s,canUseFreeze:e.freezesRemaining>0,daysUntilExpiry:0}}let t=n-1;return{isActive:t<=e.freezesRemaining,willExpireAt:null,canUseFreeze:t<=e.freezesRemaining,daysUntilExpiry:-t}}useFreeze(e,r=new Date){if(e.freezesRemaining<=0)return null;return{...e,freezesRemaining:e.freezesRemaining-1,freezeUsedAt:r}}awardMonthlyFreezes(e){return{...e,freezesRemaining:Math.min(e.freezesRemaining+this.config.freezesPerMonth,this.config.maxFreezes)}}getInitialState(){return{currentStreak:0,longestStreak:0,lastActivityAt:null,lastActivityDate:null,freezesRemaining:this.config.freezesPerMonth,freezeUsedAt:null}}getMilestones(e){let r=[3,7,14,30,60,90,180,365,500,1000],o=r.filter((t)=>e>=t),n=r.find((t)=>e<t)??null;return{achieved:o,next:n}}getDateString(e){let r=e.getFullYear(),o=String(e.getMonth()+1).padStart(2,"0"),n=String(e.getDate()).padStart(2,"0");return`${r}-${o}-${n}`}getDaysBetween(e,r){let o=new Date(e),t=new Date(r).getTime()-o.getTime();return Math.floor(t/86400000)}addDays(e,r){return new Date(e.getTime()+r*24*60*60*1000)}}var G=new m;import{defineTranslation as j}from"@contractspec/lib.contracts-spec/translations";var I=j({meta:{key:"learning-journey.messages",version:"1.0.0",domain:"learning-journey",description:"XP source labels for the learning-journey module",owners:["platform"],stability:"experimental"},locale:"en",fallback:"en",messages:{"xp.source.base":{value:"Base",description:"XP breakdown label for base XP"},"xp.source.scoreBonus":{value:"Score Bonus",description:"XP breakdown label for score-based bonus"},"xp.source.perfectScore":{value:"Perfect Score",description:"XP breakdown label for perfect score bonus"},"xp.source.firstAttempt":{value:"First Attempt",description:"XP breakdown label for first attempt bonus"},"xp.source.retryPenalty":{value:"Retry Penalty",description:"XP breakdown label for retry penalty"},"xp.source.streakBonus":{value:"Streak Bonus",description:"XP breakdown label for streak bonus"}}});import{defineTranslation as q}from"@contractspec/lib.contracts-spec/translations";var b=q({meta:{key:"learning-journey.messages",version:"1.0.0",domain:"learning-journey",description:"XP source labels (Spanish)",owners:["platform"],stability:"experimental"},locale:"es",fallback:"en",messages:{"xp.source.base":{value:"Base",description:"XP breakdown label for base XP"},"xp.source.scoreBonus":{value:"Bonificaci\xF3n por puntuaci\xF3n",description:"XP breakdown label for score-based bonus"},"xp.source.perfectScore":{value:"Puntuaci\xF3n perfecta",description:"XP breakdown label for perfect score bonus"},"xp.source.firstAttempt":{value:"Primer intento",description:"XP breakdown label for first attempt bonus"},"xp.source.retryPenalty":{value:"Penalizaci\xF3n por reintento",description:"XP breakdown label for retry penalty"},"xp.source.streakBonus":{value:"Bonificaci\xF3n por racha",description:"XP breakdown label for streak bonus"}}});import{defineTranslation as W}from"@contractspec/lib.contracts-spec/translations";var x=W({meta:{key:"learning-journey.messages",version:"1.0.0",domain:"learning-journey",description:"XP source labels (French)",owners:["platform"],stability:"experimental"},locale:"fr",fallback:"en",messages:{"xp.source.base":{value:"Base",description:"XP breakdown label for base XP"},"xp.source.scoreBonus":{value:"Bonus de score",description:"XP breakdown label for score-based bonus"},"xp.source.perfectScore":{value:"Score parfait",description:"XP breakdown label for perfect score bonus"},"xp.source.firstAttempt":{value:"Premier essai",description:"XP breakdown label for first attempt bonus"},"xp.source.retryPenalty":{value:"P\xE9nalit\xE9 de r\xE9essai",description:"XP breakdown label for retry penalty"},"xp.source.streakBonus":{value:"Bonus de s\xE9rie",description:"XP breakdown label for streak bonus"}}});import{createI18nFactory as z}from"@contractspec/lib.contracts-spec/translations";var h=z({specKey:"learning-journey.messages",catalogs:[I,x,b]}),C=h.create,ce=h.getDefault,de=h.resetRegistry;var g={baseValues:{lesson_complete:10,quiz_pass:20,quiz_perfect:50,flashcard_review:1,course_complete:200,module_complete:50,streak_bonus:5,achievement_unlock:0,daily_goal_complete:15,first_lesson:25,journey_step:5,journey_complete:50},scoreThresholds:[{min:90,multiplier:1.5},{min:80,multiplier:1.25},{min:70,multiplier:1},{min:60,multiplier:0.75},{min:0,multiplier:0.5}],streakTiers:[{days:365,bonus:50},{days:180,bonus:30},{days:90,bonus:20},{days:30,bonus:15},{days:14,bonus:10},{days:7,bonus:5},{days:3,bonus:2},{days:1,bonus:0}],perfectScoreMultiplier:1.5,firstAttemptBonus:10,retryPenalty:0.5,speedBonusMultiplier:1.2,speedBonusThreshold:0.8};class J{config;constructor(e={}){this.config={...g,...e,baseValues:{...g.baseValues,...e.baseValues},scoreThresholds:e.scoreThresholds||g.scoreThresholds,streakTiers:e.streakTiers||g.streakTiers}}calculate(e){let r=[],o=e.baseXp??this.config.baseValues[e.activity],n=o;if(r.push({source:"base",amount:o}),e.score!==void 0){let t=this.getScoreMultiplier(e.score);if(t!==1){let s=Math.round(o*(t-1));n+=s,r.push({source:"score_bonus",amount:s,multiplier:t})}if(e.score===100){let s=Math.round(o*(this.config.perfectScoreMultiplier-1));n+=s,r.push({source:"perfect_score",amount:s,multiplier:this.config.perfectScoreMultiplier})}}if(e.attemptNumber===1&&!e.isRetry)n+=this.config.firstAttemptBonus,r.push({source:"first_attempt",amount:this.config.firstAttemptBonus});if(e.isRetry){let t=Math.round(n*(1-this.config.retryPenalty));n-=t,r.push({source:"retry_penalty",amount:-t,multiplier:this.config.retryPenalty})}if(e.currentStreak&&e.currentStreak>0){let t=this.getStreakBonus(e.currentStreak);if(t>0)n+=t,r.push({source:"streak_bonus",amount:t})}if(o>0)n=Math.max(1,n);return{totalXp:Math.round(n),baseXp:o,breakdown:r}}calculateStreakBonus(e){let r=this.getStreakBonus(e);return{totalXp:r,baseXp:r,breakdown:[{source:"streak_bonus",amount:r}]}}getXpForLevel(e){if(e<=1)return 0;return Math.round(100*Math.pow(e-1,1.5))}getLevelFromXp(e){let r=1,o=this.getXpForLevel(r+1);while(e>=o&&r<1000)r++,o=this.getXpForLevel(r+1);let n=this.getXpForLevel(r),t=this.getXpForLevel(r+1);return{level:r,xpInLevel:e-n,xpForNextLevel:t-n}}getScoreMultiplier(e){for(let r of this.config.scoreThresholds)if(e>=r.min)return r.multiplier;return 1}getStreakBonus(e){for(let r of this.config.streakTiers)if(e>=r.days)return r.bonus;return 0}}var Q={base:"xp.source.base",score_bonus:"xp.source.scoreBonus",perfect_score:"xp.source.perfectScore",first_attempt:"xp.source.firstAttempt",retry_penalty:"xp.source.retryPenalty",streak_bonus:"xp.source.streakBonus"};function ae(e,r){let o=C(r),n=Q[e];return n?o.t(n):e}var ye=new J;var T=(e,r)=>{if(!e)return!0;if(!r)return!1;return Object.entries(e).every(([o,n])=>r[o]===n)},E=(e,r)=>{if(e.eventName!==r.name)return!1;if(e.eventVersion!==void 0&&r.version!==void 0&&e.eventVersion!==r.version)return!1;if(e.sourceModule&&r.sourceModule&&e.sourceModule!==r.sourceModule)return!1;return T(e.payloadFilter,r.payload)},P=(e,r,o,n)=>{if(e.kind==="count"){if(!E(e,r))return{matched:!1};let t=o.occurrences+1;return{matched:(e.withinHours===void 0||n!==void 0&&r.occurredAt!==void 0&&(r.occurredAt.getTime()-n.getTime())/3600000<=e.withinHours)&&t>=e.atLeast,occurrences:t}}if(e.kind==="time_window"){if(!E(e,r))return{matched:!1};if(e.availableAfterHours!==void 0&&n!==void 0&&r.occurredAt!==void 0&&(r.occurredAt.getTime()-n.getTime())/3600000<e.availableAfterHours)return{matched:!1};if(e.withinHoursOfStart!==void 0&&n!==void 0&&r.occurredAt!==void 0&&(r.occurredAt.getTime()-n.getTime())/3600000>e.withinHoursOfStart)return{matched:!1};return{matched:!0}}if(e.kind==="mastery"){if(r.name!==e.eventName)return{matched:!1};if(!T(e.payloadFilter,r.payload))return{matched:!1};let t=e.masteryField??"mastery",s=r.payload?.[t];if(typeof s!=="number")return{matched:!1};if(s<e.minimumMastery)return{matched:!1};let u=o.masteryCount+1;return{masteryCount:u,matched:u>=(e.requiredCount??1)}}return{matched:E(e,r)}},B=(e,r)=>{if(!e||!r)return{};let o=r.getTime(),n=o;if(e.unlockOnDay!==void 0)n=o+(e.unlockOnDay-1)*24*60*60*1000;if(e.unlockAfterHours!==void 0)n=o+e.unlockAfterHours*60*60*1000;let t=new Date(n),s=e.dueWithinHours!==void 0?new Date(t.getTime()+e.dueWithinHours*60*60*1000):void 0;return{availableAt:t,dueAt:s}},l=(e,r)=>{let o=r.steps.find((n)=>n.stepId===e.stepId);if(!o)return!1;if(e.kind==="step_completed")return o.status==="COMPLETED"||o.status==="SKIPPED";return o.selectedBranchKey===e.branchKey},w=(e,r,o,n)=>{if(!e.branches?.length)return;if(!r)return e.branches.find((t)=>t.when===void 0);for(let t of e.branches){if(!t.when)continue;if(P(t.when,r,o,n).matched)return t}return e.branches.find((t)=>t.when===void 0)};var Z=(e)=>e.map((r)=>({...r})),$=(e,r)=>{let o=new Map;for(let n of e.steps){let t=r.steps.find((u)=>u.stepId===n.id),s=n.branches?.find((u)=>u.key===t?.selectedBranchKey);if(!s?.blockStepIds?.length)continue;for(let u of s.blockStepIds)o.set(u,{blockedByBranchKey:s.key,blockedByStepId:n.id})}return o},y=(e,r,o={})=>{let n=o.now??new Date,t={...r,badges:[...r.badges],eventLog:[...r.eventLog],steps:Z(r.steps),streak:{...r.streak}},s=$(e,t);for(let u of e.steps){let c=t.steps.find((S)=>S.stepId===u.id);if(!c)continue;let{availableAt:i,dueAt:d}=B(u.availability,t.startedAt);if(c.availableAt=i,c.dueAt=d,c.status==="COMPLETED"||c.status==="SKIPPED")continue;let a=s.get(u.id);if(a){c.blockedAt??=n,c.blockedByBranchKey=a.blockedByBranchKey,c.blockedByStepId=a.blockedByStepId,c.status="BLOCKED";continue}let f=u.prerequisites??[],p=u.prerequisiteMode==="any"?f.some((S)=>l(S,t)):f.every((S)=>l(S,t)),R=(f.length===0?!0:p)&&(!i||n.getTime()>=i.getTime());if(d&&n.getTime()>d.getTime()){c.missedAt??=n,c.status="MISSED";continue}c.status=R?"AVAILABLE":"LOCKED"}return t},A=(e)=>{let r=e.steps.filter((o)=>o.status!=="BLOCKED");return r.length>0&&r.every((o)=>o.status==="COMPLETED"||o.status==="SKIPPED")},D=(e,r,o={})=>{let n=y(e,r,o),t=n.steps.filter((p)=>p.status!=="BLOCKED"),s=n.steps.filter((p)=>p.status==="COMPLETED"),u=n.steps.filter((p)=>p.status==="AVAILABLE").map((p)=>p.stepId),c=n.steps.filter((p)=>p.status==="BLOCKED").map((p)=>p.stepId),i=n.steps.filter((p)=>p.status==="MISSED").map((p)=>p.stepId),d=t.filter((p)=>p.status==="COMPLETED"||p.status==="SKIPPED").length,a=t.length>0?Math.round(d/t.length*100):0,f=u[0]??null;return{activeStepCount:t.length,availableStepIds:u,badges:[...n.badges],blockedStepIds:c,completedAt:n.completedAt,completedStepCount:d,completedStepIds:s.map((p)=>p.stepId),currentStepId:f,isCompleted:A(n),lastActivityAt:n.lastActivityAt,learnerId:n.learnerId,missedStepIds:i,nextStepId:f,progressPercent:a,startedAt:n.startedAt,steps:n.steps,streakDays:n.streak.currentStreak,totalSteps:e.steps.length,trackId:n.trackId,xpEarned:n.xpEarned}};var k=new m,L=new J,K=(e)=>({...e,badges:[...e.badges],eventLog:[...e.eventLog],steps:e.steps.map((r)=>({...r})),streak:{...e.streak}}),N=(e)=>e.reward?.xp??e.xpReward??0,M=(e,r)=>e.steps.find((o)=>o.stepId===r),O=(e,r)=>{if(r&&!e.badges.includes(r))e.badges.push(r)},H=(e,r,o,n)=>{if(!A(r)||r.completionRewardApplied)return r;let t=e.completionRewards?.xp??0;if(t>0)r.xpEarned+=n.calculate({activity:"journey_complete",baseXp:t,currentStreak:r.streak.currentStreak}).totalXp;return O(r,e.completionRewards?.badgeKey),r.completedAt=o,r.completionRewardApplied=!0,r},v=(e,r,o,n,t,s,u,c)=>{if(n.status==="COMPLETED")return;let i=N(o);if(i>0)r.xpEarned+=c.calculate({activity:"journey_step",baseXp:i,currentStreak:r.streak.currentStreak}).totalXp;n.completedAt=t,n.eventPayload=s?.payload,n.manual=u,n.status="COMPLETED",n.triggeringEvent=s?.name??"journey.step.manual",n.xpEarned=i;let d=w(o,s,n,r.startedAt);if(!d)return;if(n.selectedBranchKey=d.key,d.reward?.xp)r.xpEarned+=d.reward.xp;O(r,d.reward?.badgeKey)},le=(e,r={})=>y(e,{badges:[],completionRewardApplied:!1,eventLog:[],learnerId:r.learnerId,startedAt:r.now,steps:e.steps.map((o)=>({masteryCount:0,occurrences:0,status:"LOCKED",stepId:o.id,xpEarned:0})),streak:k.getInitialState(),trackId:e.id,xpEarned:0},{now:r.now}),Ae=(e,r,o,n={})=>{let t=n.streakEngine??k,s=n.xpEngine??L,u=o.occurredAt??new Date,c=y(e,K(r),{now:u});c.eventLog.push({...o,occurredAt:u,trackId:e.id}),c.lastActivityAt=u,c.startedAt??=u,c.streak=t.update(c.streak,u).state;for(let i of e.steps){let d=M(c,i.id);if(!d||d.status!=="AVAILABLE")continue;let a=P(i.completion,o,d,c.startedAt);if(a.occurrences!==void 0)d.occurrences=a.occurrences;if(a.masteryCount!==void 0)d.masteryCount=a.masteryCount;if(!a.matched)continue;v(e,c,i,d,u,o,!1,s)}return H(e,y(e,c,{now:u}),u,s)},ke=(e,r,o,n={})=>{let t=n.streakEngine??k,s=n.xpEngine??L,u=n.now??new Date,c=y(e,K(r),{now:u}),i=e.steps.find((a)=>a.id===o),d=M(c,o);if(!i||!d||d.status!=="AVAILABLE")return c;return c.eventLog.push({name:"journey.step.manual",occurredAt:u,trackId:e.id}),c.lastActivityAt=u,c.startedAt??=u,c.streak=t.update(c.streak,u).state,v(e,c,i,d,u,{name:"journey.step.manual",occurredAt:u},!0,s),H(e,y(e,c,{now:u}),u,s)},Ie=(e,r,o={})=>D(e,r,o);export{Ae as recordJourneyEvent,Ie as projectJourneyProgress,le as createJourneyProgressState,ke as completeJourneyStep};
@@ -0,0 +1,8 @@
1
+ import type { JourneyProgressSnapshot, JourneyProgressState, JourneyTrackSpec } from '../track-spec';
2
+ export declare const synchronizeJourneyProgressState: (track: JourneyTrackSpec, state: JourneyProgressState, options?: {
3
+ now?: Date;
4
+ }) => JourneyProgressState;
5
+ export declare const isJourneyComplete: (state: JourneyProgressState) => boolean;
6
+ export declare const buildJourneyProgressSnapshot: (track: JourneyTrackSpec, state: JourneyProgressState, options?: {
7
+ now?: Date;
8
+ }) => JourneyProgressSnapshot;
@@ -0,0 +1,2 @@
1
+ // @bun
2
+ var S=(e,t)=>{if(!e)return!0;if(!t)return!1;return Object.entries(e).every(([n,r])=>t[n]===r)},y=(e,t)=>{if(e.eventName!==t.name)return!1;if(e.eventVersion!==void 0&&t.version!==void 0&&e.eventVersion!==t.version)return!1;if(e.sourceModule&&t.sourceModule&&e.sourceModule!==t.sourceModule)return!1;return S(e.payloadFilter,t.payload)},J=(e,t,n,r)=>{if(e.kind==="count"){if(!y(e,t))return{matched:!1};let s=n.occurrences+1;return{matched:(e.withinHours===void 0||r!==void 0&&t.occurredAt!==void 0&&(t.occurredAt.getTime()-r.getTime())/3600000<=e.withinHours)&&s>=e.atLeast,occurrences:s}}if(e.kind==="time_window"){if(!y(e,t))return{matched:!1};if(e.availableAfterHours!==void 0&&r!==void 0&&t.occurredAt!==void 0&&(t.occurredAt.getTime()-r.getTime())/3600000<e.availableAfterHours)return{matched:!1};if(e.withinHoursOfStart!==void 0&&r!==void 0&&t.occurredAt!==void 0&&(t.occurredAt.getTime()-r.getTime())/3600000>e.withinHoursOfStart)return{matched:!1};return{matched:!0}}if(e.kind==="mastery"){if(t.name!==e.eventName)return{matched:!1};if(!S(e.payloadFilter,t.payload))return{matched:!1};let s=e.masteryField??"mastery",a=t.payload?.[s];if(typeof a!=="number")return{matched:!1};if(a<e.minimumMastery)return{matched:!1};let u=n.masteryCount+1;return{masteryCount:u,matched:u>=(e.requiredCount??1)}}return{matched:y(e,t)}},h=(e,t)=>{if(!e||!t)return{};let n=t.getTime(),r=n;if(e.unlockOnDay!==void 0)r=n+(e.unlockOnDay-1)*24*60*60*1000;if(e.unlockAfterHours!==void 0)r=n+e.unlockAfterHours*60*60*1000;let s=new Date(r),a=e.dueWithinHours!==void 0?new Date(s.getTime()+e.dueWithinHours*60*60*1000):void 0;return{availableAt:s,dueAt:a}},m=(e,t)=>{let n=t.steps.find((r)=>r.stepId===e.stepId);if(!n)return!1;if(e.kind==="step_completed")return n.status==="COMPLETED"||n.status==="SKIPPED";return n.selectedBranchKey===e.branchKey},E=(e,t,n,r)=>{if(!e.branches?.length)return;if(!t)return e.branches.find((s)=>s.when===void 0);for(let s of e.branches){if(!s.when)continue;if(J(s.when,t,n,r).matched)return s}return e.branches.find((s)=>s.when===void 0)};var b=(e)=>e.map((t)=>({...t})),k=(e,t)=>{let n=new Map;for(let r of e.steps){let s=t.steps.find((u)=>u.stepId===r.id),a=r.branches?.find((u)=>u.key===s?.selectedBranchKey);if(!a?.blockStepIds?.length)continue;for(let u of a.blockStepIds)n.set(u,{blockedByBranchKey:a.key,blockedByStepId:r.id})}return n},P=(e,t,n={})=>{let r=n.now??new Date,s={...t,badges:[...t.badges],eventLog:[...t.eventLog],steps:b(t.steps),streak:{...t.streak}},a=k(e,s);for(let u of e.steps){let c=s.steps.find((f)=>f.stepId===u.id);if(!c)continue;let{availableAt:p,dueAt:d}=h(u.availability,s.startedAt);if(c.availableAt=p,c.dueAt=d,c.status==="COMPLETED"||c.status==="SKIPPED")continue;let l=a.get(u.id);if(l){c.blockedAt??=r,c.blockedByBranchKey=l.blockedByBranchKey,c.blockedByStepId=l.blockedByStepId,c.status="BLOCKED";continue}let i=u.prerequisites??[],o=u.prerequisiteMode==="any"?i.some((f)=>m(f,s)):i.every((f)=>m(f,s)),g=(i.length===0?!0:o)&&(!p||r.getTime()>=p.getTime());if(d&&r.getTime()>d.getTime()){c.missedAt??=r,c.status="MISSED";continue}c.status=g?"AVAILABLE":"LOCKED"}return s},A=(e)=>{let t=e.steps.filter((n)=>n.status!=="BLOCKED");return t.length>0&&t.every((n)=>n.status==="COMPLETED"||n.status==="SKIPPED")},w=(e,t,n={})=>{let r=P(e,t,n),s=r.steps.filter((o)=>o.status!=="BLOCKED"),a=r.steps.filter((o)=>o.status==="COMPLETED"),u=r.steps.filter((o)=>o.status==="AVAILABLE").map((o)=>o.stepId),c=r.steps.filter((o)=>o.status==="BLOCKED").map((o)=>o.stepId),p=r.steps.filter((o)=>o.status==="MISSED").map((o)=>o.stepId),d=s.filter((o)=>o.status==="COMPLETED"||o.status==="SKIPPED").length,l=s.length>0?Math.round(d/s.length*100):0,i=u[0]??null;return{activeStepCount:s.length,availableStepIds:u,badges:[...r.badges],blockedStepIds:c,completedAt:r.completedAt,completedStepCount:d,completedStepIds:a.map((o)=>o.stepId),currentStepId:i,isCompleted:A(r),lastActivityAt:r.lastActivityAt,learnerId:r.learnerId,missedStepIds:p,nextStepId:i,progressPercent:l,startedAt:r.startedAt,steps:r.steps,streakDays:r.streak.currentStreak,totalSteps:e.steps.length,trackId:r.trackId,xpEarned:r.xpEarned}};export{P as synchronizeJourneyProgressState,A as isJourneyComplete,w as buildJourneyProgressSnapshot};
@@ -1,125 +1,156 @@
1
- export interface BaseEventConditionSpec {
2
- /**
3
- * Required event name to satisfy the condition.
4
- */
1
+ import type { StreakState } from './engines/streak';
2
+ export interface JourneyBaseEventConditionSpec {
5
3
  eventName: string;
6
- /**
7
- * Optional event version to match.
8
- */
9
4
  eventVersion?: number;
10
- /**
11
- * Optional source module to match (for disambiguation).
12
- */
13
5
  sourceModule?: string;
14
- /**
15
- * Optional payload filter (shallow equality on keys).
16
- */
17
6
  payloadFilter?: Record<string, unknown>;
18
7
  }
19
- export interface EventCompletionConditionSpec extends BaseEventConditionSpec {
8
+ export interface JourneyEventConditionSpec extends JourneyBaseEventConditionSpec {
20
9
  kind?: 'event';
21
10
  }
22
- export interface CountCompletionConditionSpec extends BaseEventConditionSpec {
11
+ export interface JourneyCountConditionSpec extends JourneyBaseEventConditionSpec {
23
12
  kind: 'count';
24
- /**
25
- * Minimum number of matching events required to complete the step.
26
- */
27
13
  atLeast: number;
28
- /**
29
- * Optional time window (hours) from track start for counting.
30
- */
31
14
  withinHours?: number;
32
15
  }
33
- export interface TimeWindowCompletionConditionSpec extends BaseEventConditionSpec {
16
+ export interface JourneyTimeWindowConditionSpec extends JourneyBaseEventConditionSpec {
34
17
  kind: 'time_window';
35
- /**
36
- * Must be completed within this window (hours) from track start.
37
- */
38
18
  withinHoursOfStart?: number;
39
- /**
40
- * Optional additional delay before the step becomes available (hours).
41
- */
42
19
  availableAfterHours?: number;
43
20
  }
44
- export interface SrsMasteryCompletionConditionSpec {
45
- kind: 'srs_mastery';
46
- /**
47
- * Event carrying mastery info (defaults to drill/flashcard mastery events).
48
- */
21
+ export interface JourneyMasteryConditionSpec {
22
+ kind: 'mastery';
49
23
  eventName: string;
50
- /**
51
- * Payload key containing skill identifier; defaults to `skillId`.
52
- */
53
24
  skillIdField?: string;
54
- /**
55
- * Payload key containing mastery value; defaults to `mastery`.
56
- */
57
25
  masteryField?: string;
58
- /**
59
- * Minimum mastery value required (e.g., 0-1 or a numeric level).
60
- */
61
26
  minimumMastery: number;
62
- /**
63
- * Optional number of mastered cards required to complete step.
64
- */
65
27
  requiredCount?: number;
66
- /**
67
- * Optional payload filter.
68
- */
69
28
  payloadFilter?: Record<string, unknown>;
70
29
  }
71
- export type StepCompletionConditionSpec = EventCompletionConditionSpec | CountCompletionConditionSpec | TimeWindowCompletionConditionSpec | SrsMasteryCompletionConditionSpec;
72
- export interface StepAvailabilitySpec {
73
- /**
74
- * Unlock step after a delay (hours) from track start.
75
- */
30
+ export type JourneyConditionSpec = JourneyCountConditionSpec | JourneyEventConditionSpec | JourneyMasteryConditionSpec | JourneyTimeWindowConditionSpec;
31
+ export interface JourneyAvailabilitySpec {
76
32
  unlockAfterHours?: number;
77
- /**
78
- * Unlock on a specific day from track start (day 1 = start day).
79
- */
80
33
  unlockOnDay?: number;
81
- /**
82
- * Optional due window (hours) from unlock; if exceeded, step is considered missed.
83
- */
84
34
  dueWithinHours?: number;
85
35
  }
86
- export interface StreakRuleSpec {
87
- hoursWindow?: number;
88
- bonusXp?: number;
89
- }
90
- export interface CompletionRewardsSpec {
91
- xpBonus?: number;
36
+ export interface JourneyRewardSpec {
92
37
  badgeKey?: string;
38
+ xp?: number;
93
39
  }
94
- export interface LearningJourneyStepSpec {
95
- id: string;
96
- title: string;
40
+ export interface JourneyPrerequisiteSpec {
41
+ kind: 'branch_selected' | 'step_completed';
42
+ branchKey?: string;
43
+ stepId: string;
44
+ }
45
+ export interface JourneyBranchSpec {
46
+ key: string;
47
+ blockStepIds?: string[];
48
+ label?: string;
49
+ metadata?: Record<string, unknown>;
50
+ reward?: JourneyRewardSpec;
51
+ when?: JourneyConditionSpec;
52
+ }
53
+ export interface JourneyStreakRuleSpec {
54
+ bonusXp?: number;
55
+ hoursWindow?: number;
56
+ }
57
+ export interface JourneyStepSpec {
58
+ actionLabel?: string;
59
+ actionUrl?: string;
60
+ availability?: JourneyAvailabilitySpec;
61
+ branches?: JourneyBranchSpec[];
62
+ canSkip?: boolean;
63
+ completion: JourneyConditionSpec;
97
64
  description?: string;
98
- instructions?: string;
99
65
  helpUrl?: string;
100
- order?: number;
101
- completion: StepCompletionConditionSpec;
102
- availability?: StepAvailabilitySpec;
103
- xpReward?: number;
66
+ id: string;
67
+ instructions?: string;
104
68
  isRequired?: boolean;
105
- canSkip?: boolean;
106
- actionUrl?: string;
107
- actionLabel?: string;
108
69
  metadata?: Record<string, unknown>;
70
+ order?: number;
71
+ prerequisiteMode?: 'all' | 'any';
72
+ prerequisites?: JourneyPrerequisiteSpec[];
73
+ reward?: JourneyRewardSpec;
74
+ title: string;
75
+ xpReward?: number;
109
76
  }
110
- export interface LearningJourneyTrackSpec {
111
- id: string;
112
- productId?: string;
113
- name: string;
77
+ export interface JourneyTrackSpec {
78
+ canSkip?: boolean;
79
+ completionRewards?: JourneyRewardSpec;
114
80
  description?: string;
115
- targetUserSegment?: string;
116
- targetRole?: string;
117
- totalXp?: number;
81
+ id: string;
118
82
  isActive?: boolean;
119
83
  isRequired?: boolean;
120
- canSkip?: boolean;
121
- streakRule?: StreakRuleSpec;
122
- completionRewards?: CompletionRewardsSpec;
123
- steps: LearningJourneyStepSpec[];
124
84
  metadata?: Record<string, unknown>;
85
+ name: string;
86
+ productId?: string;
87
+ steps: JourneyStepSpec[];
88
+ streakRule?: JourneyStreakRuleSpec;
89
+ targetRole?: string;
90
+ targetUserSegment?: string;
91
+ totalXp?: number;
92
+ }
93
+ export interface JourneyEvent {
94
+ learnerId?: string;
95
+ name: string;
96
+ occurredAt?: Date;
97
+ payload?: Record<string, unknown>;
98
+ sourceModule?: string;
99
+ trackId?: string;
100
+ version?: number;
101
+ }
102
+ export type JourneyStepStatus = 'AVAILABLE' | 'BLOCKED' | 'COMPLETED' | 'LOCKED' | 'MISSED' | 'SKIPPED';
103
+ export interface JourneyStepProgressState {
104
+ availableAt?: Date;
105
+ blockedAt?: Date;
106
+ blockedByBranchKey?: string;
107
+ blockedByStepId?: string;
108
+ completedAt?: Date;
109
+ dueAt?: Date;
110
+ eventPayload?: Record<string, unknown>;
111
+ manual?: boolean;
112
+ masteryCount: number;
113
+ missedAt?: Date;
114
+ occurrences: number;
115
+ selectedBranchKey?: string;
116
+ skippedAt?: Date;
117
+ status: JourneyStepStatus;
118
+ stepId: string;
119
+ triggeringEvent?: string;
120
+ xpEarned: number;
121
+ }
122
+ export interface JourneyProgressState {
123
+ badges: string[];
124
+ completedAt?: Date;
125
+ completionRewardApplied: boolean;
126
+ eventLog: JourneyEvent[];
127
+ lastActivityAt?: Date;
128
+ learnerId?: string;
129
+ startedAt?: Date;
130
+ steps: JourneyStepProgressState[];
131
+ streak: StreakState;
132
+ trackId: string;
133
+ xpEarned: number;
134
+ }
135
+ export interface JourneyProgressSnapshot {
136
+ activeStepCount: number;
137
+ availableStepIds: string[];
138
+ badges: string[];
139
+ blockedStepIds: string[];
140
+ completedAt?: Date;
141
+ completedStepCount: number;
142
+ completedStepIds: string[];
143
+ currentStepId: string | null;
144
+ isCompleted: boolean;
145
+ lastActivityAt?: Date;
146
+ learnerId?: string;
147
+ missedStepIds: string[];
148
+ nextStepId: string | null;
149
+ progressPercent: number;
150
+ startedAt?: Date;
151
+ steps: JourneyStepProgressState[];
152
+ streakDays: number;
153
+ totalSteps: number;
154
+ trackId: string;
155
+ xpEarned: number;
125
156
  }