@contractspec/module.learning-journey 3.7.18 → 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.
- package/README.md +3 -2
- package/dist/browser/contracts/index.js +1 -1
- package/dist/browser/contracts/journey.js +1 -0
- package/dist/browser/contracts/operations.js +1 -1
- package/dist/browser/docs/index.js +13 -12
- package/dist/browser/docs/learning-journey.docblock.js +13 -12
- package/dist/browser/engines/index.js +1 -1
- package/dist/browser/engines/xp.js +1 -1
- package/dist/browser/entities/index.js +1 -1
- package/dist/browser/entities/journey.js +1 -0
- package/dist/browser/index.js +14 -13
- package/dist/browser/learning-journey.feature.js +1 -1
- package/dist/browser/runtime/index.js +1 -0
- package/dist/browser/runtime/matchers.js +1 -0
- package/dist/browser/runtime/progress-state.js +1 -0
- package/dist/browser/runtime/snapshot.js +1 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/index.js +1 -1
- package/dist/contracts/{onboarding.d.ts → journey.d.ts} +477 -180
- package/dist/contracts/journey.js +2 -0
- package/dist/contracts/operations.js +1 -1
- package/dist/docs/index.js +13 -12
- package/dist/docs/learning-journey.docblock.js +13 -12
- package/dist/engines/index.js +1 -1
- package/dist/engines/xp.d.ts +1 -1
- package/dist/engines/xp.js +1 -1
- package/dist/entities/index.d.ts +28 -27
- package/dist/entities/index.js +1 -1
- package/dist/entities/{onboarding.d.ts → journey.d.ts} +61 -74
- package/dist/entities/journey.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +14 -13
- package/dist/learning-journey.feature.js +1 -1
- package/dist/node/contracts/index.js +1 -1
- package/dist/node/contracts/journey.js +1 -0
- package/dist/node/contracts/operations.js +1 -1
- package/dist/node/docs/index.js +13 -12
- package/dist/node/docs/learning-journey.docblock.js +13 -12
- package/dist/node/engines/index.js +1 -1
- package/dist/node/engines/xp.js +1 -1
- package/dist/node/entities/index.js +1 -1
- package/dist/node/entities/journey.js +1 -0
- package/dist/node/index.js +14 -13
- package/dist/node/learning-journey.feature.js +1 -1
- package/dist/node/runtime/index.js +1 -0
- package/dist/node/runtime/matchers.js +1 -0
- package/dist/node/runtime/progress-state.js +1 -0
- package/dist/node/runtime/snapshot.js +1 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/journey-runtime.test.d.ts +1 -0
- package/dist/runtime/matchers.d.ts +20 -0
- package/dist/runtime/matchers.js +2 -0
- package/dist/runtime/progress-state.d.ts +19 -0
- package/dist/runtime/progress-state.js +2 -0
- package/dist/runtime/snapshot.d.ts +8 -0
- package/dist/runtime/snapshot.js +2 -0
- package/dist/track-spec.d.ts +118 -87
- package/package.json +86 -30
- package/dist/browser/contracts/onboarding.js +0 -1
- package/dist/browser/entities/onboarding.js +0 -1
- package/dist/contracts/onboarding.js +0 -2
- package/dist/entities/onboarding.js +0 -2
- package/dist/node/contracts/onboarding.js +0 -1
- 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};
|
package/dist/track-spec.d.ts
CHANGED
|
@@ -1,125 +1,156 @@
|
|
|
1
|
-
|
|
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
|
|
8
|
+
export interface JourneyEventConditionSpec extends JourneyBaseEventConditionSpec {
|
|
20
9
|
kind?: 'event';
|
|
21
10
|
}
|
|
22
|
-
export interface
|
|
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
|
|
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
|
|
45
|
-
kind: '
|
|
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
|
|
72
|
-
export interface
|
|
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
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
name: string;
|
|
77
|
+
export interface JourneyTrackSpec {
|
|
78
|
+
canSkip?: boolean;
|
|
79
|
+
completionRewards?: JourneyRewardSpec;
|
|
114
80
|
description?: string;
|
|
115
|
-
|
|
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
|
}
|