Feature detail
Planning / goal abstraction
Ordered phases, local-date deadlines, and recommendation roles let consumer apps turn progress counts into a clear next track.
Worked example
Goal evaluation
The core turns count-based progress into recommendation state without knowing whether the units are concepts or sections.
Input
{
"plan": {
"id": "demo-plan",
"label": "Exam 3 then Final",
"phases": [
{
"id": "exam3-clean-sweep",
"label": "Perfect Exam 3 first",
"trackId": "exam3",
"deadlineLocalDate": "2026-04-18",
"deadlineBehavior": "stay_primary_until_complete"
},
{
"id": "final-breadth-sweep",
"label": "Then hit the final",
"trackId": "final",
"deadlineLocalDate": "2026-04-27",
"deadlineBehavior": "advance_after_deadline"
}
]
},
"snapshots": [
{
"phaseId": "exam3-clean-sweep",
"trackId": "exam3",
"completedUnits": 8,
"totalUnits": 12
},
{
"phaseId": "final-breadth-sweep",
"trackId": "final",
"completedUnits": 3,
"totalUnits": 14
}
],
"context": {
"localDate": "2026-04-16"
}
}Output
{
"plan": {
"id": "demo-plan",
"label": "Exam 3 then Final",
"phases": [
{
"id": "exam3-clean-sweep",
"label": "Perfect Exam 3 first",
"trackId": "exam3",
"deadlineLocalDate": "2026-04-18",
"deadlineBehavior": "stay_primary_until_complete"
},
{
"id": "final-breadth-sweep",
"label": "Then hit the final",
"trackId": "final",
"deadlineLocalDate": "2026-04-27",
"deadlineBehavior": "advance_after_deadline"
}
]
},
"localDate": "2026-04-16",
"phases": [
{
"id": "exam3-clean-sweep",
"label": "Perfect Exam 3 first",
"trackId": "exam3",
"deadlineLocalDate": "2026-04-18",
"deadlineBehavior": "stay_primary_until_complete",
"completedUnits": 8,
"totalUnits": 12,
"targetCompletedUnits": 12,
"remainingUnits": 4,
"progressRatio": 0.6666666666666666,
"isComplete": false,
"isActive": true,
"timeStatus": "upcoming",
"daysUntilDeadline": 2,
"daysFromDeadline": 0,
"recommendationRole": "primary"
},
{
"id": "final-breadth-sweep",
"label": "Then hit the final",
"trackId": "final",
"deadlineLocalDate": "2026-04-27",
"deadlineBehavior": "advance_after_deadline",
"completedUnits": 3,
"totalUnits": 14,
"targetCompletedUnits": 14,
"remainingUnits": 11,
"progressRatio": 0.21428571428571427,
"isComplete": false,
"isActive": false,
"timeStatus": "upcoming",
"daysUntilDeadline": 11,
"daysFromDeadline": 0,
"recommendationRole": "queued"
}
],
"activePhase": {
"id": "exam3-clean-sweep",
"label": "Perfect Exam 3 first",
"trackId": "exam3",
"deadlineLocalDate": "2026-04-18",
"deadlineBehavior": "stay_primary_until_complete",
"completedUnits": 8,
"totalUnits": 12,
"targetCompletedUnits": 12,
"remainingUnits": 4,
"progressRatio": 0.6666666666666666,
"isComplete": false,
"isActive": true,
"timeStatus": "upcoming",
"daysUntilDeadline": 2,
"daysFromDeadline": 0,
"recommendationRole": "primary"
},
"trackPriority": [
"exam3",
"final"
]
}Real source excerpt
Goal types and evaluator
export type GoalDeadlineBehavior =
| 'stay_primary_until_complete'
| 'advance_after_deadline'
export type GoalTimeStatus = 'none' | 'upcoming' | 'today' | 'past_due'
export type GoalRecommendationRole = 'primary' | 'catch_up' | 'queued' | 'complete'
export interface GoalPhaseDefinition<TTrackId extends string = string> {
id: string
label: string
trackId: TTrackId
description?: string
deadlineLocalDate?: string
deadlineBehavior?: GoalDeadlineBehavior
targetCompletedUnits?: number
}
export interface GoalPlan<TTrackId extends string = string> {
id: string
label: string
phases: readonly GoalPhaseDefinition<TTrackId>[]
}
export interface GoalPhaseSnapshot<TTrackId extends string = string> {
phaseId: string
trackId: TTrackId
completedUnits: number
totalUnits: number
}
export interface GoalEvaluationContext {
localDate?: string
}
export interface GoalPhaseState<TTrackId extends string = string>
extends GoalPhaseDefinition<TTrackId> {
completedUnits: number
totalUnits: number
targetCompletedUnits: number
remainingUnits: number
progressRatio: number
isComplete: boolean
isActive: boolean
deadlineBehavior: GoalDeadlineBehavior
timeStatus: GoalTimeStatus
daysUntilDeadline: number | null
daysFromDeadline: number | null
recommendationRole: GoalRecommendationRole
}
export interface GoalPlanEvaluation<TTrackId extends string = string> {
plan: GoalPlan<TTrackId>
localDate: string | null
phases: GoalPhaseState<TTrackId>[]
activePhase: GoalPhaseState<TTrackId> | null
trackPriority: TTrackId[]
}
export const DEFAULT_GOAL_DEADLINE_BEHAVIOR: GoalDeadlineBehavior = 'stay_primary_until_complete'
const LOCAL_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/
const ROLE_PRIORITY: Record<GoalRecommendationRole, number> = {
primary: 0,
catch_up: 1,
queued: 2,
complete: 3,
}
const assertNonNegativeInteger = (value: number, label: string): number => {
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
throw new Error(`${label} must be a non-negative integer`)
}
return value
}
const parseGoalLocalDate = (value: string, label: string): number => {
if (!LOCAL_DATE_PATTERN.test(value)) {
throw new Error(`${label} must use YYYY-MM-DD format`)
}
const [yearToken, monthToken, dayToken] = value.split('-')
const year = Number(yearToken)
const month = Number(monthToken)
const day = Number(dayToken)
const utc = Date.UTC(year, month - 1, day)
const parsed = new Date(utc)
if (
Number.isNaN(parsed.getTime())
|| parsed.getUTCFullYear() !== year
|| parsed.getUTCMonth() !== month - 1
|| parsed.getUTCDate() !== day
) {
throw new Error(`${label} must be a real calendar date`)
}
return utc
}
const compareGoalLocalDates = (left: string, right: string): number => {
const leftUtc = parseGoalLocalDate(left, 'localDate')
const rightUtc = parseGoalLocalDate(right, 'deadlineLocalDate')
const MS_PER_DAY = 86_400_000
return Math.round((leftUtc - rightUtc) / MS_PER_DAY)
}
const resolveTimeStatus = <TTrackId extends string>(
phase: GoalPhaseDefinition<TTrackId>,
context: GoalEvaluationContext
): Pick<GoalPhaseState<TTrackId>, 'timeStatus' | 'daysUntilDeadline' | 'daysFromDeadline'> => {
if (!phase.deadlineLocalDate) {
return {
timeStatus: 'none',
daysUntilDeadline: null,
daysFromDeadline: null,
}
}
parseGoalLocalDate(phase.deadlineLocalDate, 'deadlineLocalDate')
if (!context.localDate) {
return {
timeStatus: 'none',
daysUntilDeadline: null,
daysFromDeadline: null,
}
}
const dayDelta = compareGoalLocalDates(context.localDate, phase.deadlineLocalDate)
if (dayDelta < 0) {
return {
timeStatus: 'upcoming',
daysUntilDeadline: Math.abs(dayDelta),
daysFromDeadline: 0,
}
}
if (dayDelta === 0) {
return {
timeStatus: 'today',
daysUntilDeadline: 0,
daysFromDeadline: 0,
}
}
return {
timeStatus: 'past_due',
daysUntilDeadline: 0,
daysFromDeadline: dayDelta,
}
}
const distinctTrackPriority = <TTrackId extends string>(
phases: readonly GoalPhaseState<TTrackId>[]
): TTrackId[] => {
const orderedPhases = phases
.slice()
.sort((left, right) => {
if (left.recommendationRole !== right.recommendationRole) {
return ROLE_PRIORITY[left.recommendationRole] - ROLE_PRIORITY[right.recommendationRole]
}
return 0
})
const seen = new Set<TTrackId>()
const priority: TTrackId[] = []
for (const phase of orderedPhases) {
if (phase.recommendationRole === 'complete' || seen.has(phase.trackId)) {
continue
}
seen.add(phase.trackId)
priority.push(phase.trackId)
}
return priority
}
export function resolveGoalLocalDate(
now: Date | number | string = new Date(),
timeZone = 'UTC'
): string {
const date = now instanceof Date ? now : new Date(now)
if (Number.isNaN(date.getTime())) {
throw new Error('resolveGoalLocalDate received an invalid date input')
}
const formatter = new Intl.DateTimeFormat('en-US', {
timeZone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
const parts = formatter.formatToParts(date)
const year = parts.find((part) => part.type === 'year')?.value
const month = parts.find((part) => part.type === 'month')?.value
const day = parts.find((part) => part.type === 'day')?.value
if (!year || !month || !day) {
throw new Error('resolveGoalLocalDate could not resolve a local date')
}
return `${year}-${month}-${day}`
}
export function evaluateGoalPlan<TTrackId extends string>(
plan: GoalPlan<TTrackId>,
snapshots: readonly GoalPhaseSnapshot<TTrackId>[],
context: GoalEvaluationContext = {}
): GoalPlanEvaluation<TTrackId> {
if (context.localDate) {
parseGoalLocalDate(context.localDate, 'localDate')
}
const phaseIds = new Set<string>()
for (const phase of plan.phases) {
if (phaseIds.has(phase.id)) {
throw new Error(`Duplicate goal phase id: ${phase.id}`)
}
phaseIds.add(phase.id)
if (phase.deadlineLocalDate) {
parseGoalLocalDate(phase.deadlineLocalDate, 'deadlineLocalDate')
}
if (phase.targetCompletedUnits !== undefined) {
assertNonNegativeInteger(phase.targetCompletedUnits, `targetCompletedUnits for phase ${phase.id}`)
}
}
const snapshotMap = new Map<string, GoalPhaseSnapshot<TTrackId>>()
for (const snapshot of snapshots) {
if (snapshotMap.has(snapshot.phaseId)) {
throw new Error(`Duplicate goal phase snapshot id: ${snapshot.phaseId}`)
}
assertNonNegativeInteger(snapshot.completedUnits, `completedUnits for phase ${snapshot.phaseId}`)
assertNonNegativeInteger(snapshot.totalUnits, `totalUnits for phase ${snapshot.phaseId}`)
snapshotMap.set(snapshot.phaseId, snapshot)
}
const baseStates = plan.phases.map((phase) => {
const snapshot = snapshotMap.get(phase.id)
if (snapshot && snapshot.trackId !== phase.trackId) {
throw new Error(`Goal phase snapshot track mismatch for phase ${phase.id}`)
}
const completedUnits = snapshot?.completedUnits ?? 0
const totalUnits = snapshot?.totalUnits ?? 0
const targetCompletedUnits = phase.targetCompletedUnits ?? totalUnits
const remainingUnits = Math.max(targetCompletedUnits - completedUnits, 0)
const progressRatio = targetCompletedUnits <= 0
? 0
: Math.min(completedUnits / targetCompletedUnits, 1)
const isComplete = completedUnits >= targetCompletedUnits
const deadlineBehavior = phase.deadlineBehavior ?? DEFAULT_GOAL_DEADLINE_BEHAVIOR
const timeStatus = resolveTimeStatus(phase, context)
return {
...phase,
completedUnits,
totalUnits,
targetCompletedUnits,
remainingUnits,
progressRatio,
isComplete,
isActive: false,
deadlineBehavior,
timeStatus: timeStatus.timeStatus,
daysUntilDeadline: timeStatus.daysUntilDeadline,
daysFromDeadline: timeStatus.daysFromDeadline,
recommendationRole: isComplete ? 'complete' : 'queued',
} satisfies GoalPhaseState<TTrackId>
})
const firstIncompleteIndex = baseStates.findIndex((phase) => !phase.isComplete)
let primaryIndex = -1
const catchUpIndexes = new Set<number>()
if (firstIncompleteIndex >= 0) {
let candidateIndex = firstIncompleteIndex
while (candidateIndex >= 0) {
const candidatePhase = baseStates[candidateIndex]
if (!candidatePhase) {
throw new Error('Goal planner could not resolve an incomplete phase')
}
const shouldAdvance = (
candidatePhase.timeStatus === 'past_due'
&& candidatePhase.deadlineBehavior === 'advance_after_deadline'
)
if (!shouldAdvance) {
primaryIndex = candidateIndex
break
}
const nextIncompleteIndex = baseStates.findIndex((phase, index) => (
index > candidateIndex && !phase.isComplete
))
if (nextIncompleteIndex < 0) {
primaryIndex = candidateIndex
break
}
catchUpIndexes.add(candidateIndex)
candidateIndex = nextIncompleteIndex
}
}
const evaluatedPhases = baseStates.map((phase, index) => {
if (phase.isComplete) {
return phase
}
if (index === primaryIndex) {
return {
...phase,
isActive: true,
recommendationRole: 'primary' as const,
}
}
if (catchUpIndexes.has(index)) {
return {
...phase,
recommendationRole: 'catch_up' as const,
}
}
return {
...phase,
recommendationRole: 'queued' as const,
}
})
const activePhase = evaluatedPhases.find((phase) => phase.recommendationRole === 'primary') ?? null
return {
plan,
localDate: context.localDate ?? null,
phases: evaluatedPhases,
activePhase,
trackPriority: distinctTrackPriority(evaluatedPhases),
}
}Consumer example
Stats goal dashboard adapter
Stats converts concept snapshots into phase snapshots and leaves grade math plus launcher copy local.
Real source excerpt
Stats study-goal adapter
export const studyGoalPlan: GoalPlan<ExamId> = {
id: 'a-threshold-exam3-then-final',
label: 'A-threshold sprint',
phases: [
{
id: 'exam3-clean-sweep',
label: 'Perfect Exam 3 first',
trackId: 'exam3',
description: 'Stay on Exam 3 until every concept has at least one clean independent proof.',
},
{
id: 'final-breadth-sweep',
label: 'Then hit everything on the Final',
trackId: 'final',
description: 'Once Exam 3 is covered cleanly, rotate through every cumulative final section.',
},
],
}
const studyGoalPhaseIds = new Set<StudyGoalPhaseId>(['exam3-clean-sweep', 'final-breadth-sweep'])
const emptyStorageReader: Pick<Storage, 'getItem'> = {
getItem: (_key: string) => null,
}
const normalizeStudyGoalDeadlineBehavior = (
deadlineBehavior?: GoalDeadlineBehavior | LegacyGoalDeadlineBehavior
): GoalDeadlineBehavior | undefined => {
if (deadlineBehavior === 'advance_after_deadline') return deadlineBehavior
if (deadlineBehavior === 'stay_primary_until_complete') return deadlineBehavior
if (deadlineBehavior === 'demote_to_catch_up_after_deadline') return 'advance_after_deadline'
return undefined
}
const readStoredExamProgress = (
storage: Pick<Storage, 'getItem'>,
examId: ExamId
): StoredExamProgress => {
const raw = storage.getItem(`${SESSION_STORAGE_PREFIX}:${examId}`)
const conceptIds = getExamConceptIds(examId)
if (!raw) {
return {
conceptProgress: mergeConceptProgress(conceptIds, {}),
currentTurn: 1,
}
}
try {
const parsed = JSON.parse(raw) as SessionEnvelopeLike
return {
conceptProgress: mergeConceptProgress(conceptIds, parsed.conceptProgress),
currentTurn: typeof parsed.currentTurn === 'number' && Number.isFinite(parsed.currentTurn)
? parsed.currentTurn
: 1,
}
} catch {
return {
conceptProgress: mergeConceptProgress(conceptIds, {}),
currentTurn: 1,
}
}
}
const countDueConcepts = (progress: ConceptProgressMap, currentTurn: number): number => Object.values(progress)
.filter((concept) => concept.nextEligibleTurn <= currentTurn && !concept.mastered)
.length
export const buildStudyGoalExamSnapshot = (
examId: ExamId,
conceptProgress: ConceptProgressMap,
currentTurn: number
): StudyGoalExamSnapshot => {
const concepts = Object.values(conceptProgress)
return {
examId,
totalConcepts: concepts.length,
cleanProofConcepts: concepts.filter((concept) => concept.proofCount > 0).length,
touchedConcepts: concepts.filter((concept) => (
concept.attempts > 0
|| concept.proofCount > 0
|| concept.supportedEvidenceCount > 0
|| concept.supplementalExposureCount > 0
)).length,
supportedConcepts: concepts.filter((concept) => concept.supportedEvidenceCount > 0 || concept.proofCount > 0).length,
masteredConcepts: concepts.filter((concept) => concept.mastered).length,
dueConcepts: countDueConcepts(conceptProgress, currentTurn),
}
}
const buildPhaseSnapshots = (
examSnapshots: Record<ExamId, StudyGoalExamSnapshot>
): GoalPhaseSnapshot<ExamId>[] => [
{
phaseId: 'exam3-clean-sweep',
trackId: 'exam3',
completedUnits: examSnapshots.exam3.cleanProofConcepts,
totalUnits: examSnapshots.exam3.totalConcepts,
},
{
phaseId: 'final-breadth-sweep',
trackId: 'final',
completedUnits: examSnapshots.final.touchedConcepts,
totalUnits: examSnapshots.final.totalConcepts,
},
]