diff --git a/src/app/[exam]/assess/[domain]/AssessmentClient.tsx b/src/app/[exam]/assess/[domain]/AssessmentClient.tsx
index ce669c8..e4e6f8e 100644
--- a/src/app/[exam]/assess/[domain]/AssessmentClient.tsx
+++ b/src/app/[exam]/assess/[domain]/AssessmentClient.tsx
@@ -10,6 +10,7 @@ import { Badge } from '@/components/ui/badge';
import { QuestionCard } from '@/components/assess/QuestionCard';
import { Clock, CheckCircle, XCircle, Target } from 'lucide-react';
import type { Question } from '@/types/domain';
+import { MASTERY_THRESHOLD, APPROACHING_THRESHOLD } from '@/lib/constants';
import type { QuestionAnswer, AssessmentResult } from '@/types/assessment';
interface AssessmentClientProps {
@@ -153,8 +154,8 @@ export function AssessmentClient({ examId, domainId, topicId, questions }: Asses
- = 85 ? 'default' : result.score >= 60 ? 'secondary' : 'destructive'} className="text-base px-4 py-2">
- {result.score >= 85 ? 'Excellent!' : result.score >= 60 ? 'Good Progress' : 'Needs Review'}
+ = MASTERY_THRESHOLD ? 'default' : result.score >= APPROACHING_THRESHOLD ? 'secondary' : 'destructive'} className="text-base px-4 py-2">
+ {result.score >= MASTERY_THRESHOLD ? 'Excellent!' : result.score >= APPROACHING_THRESHOLD ? 'Good Progress' : 'Needs Review'}
diff --git a/src/components/dashboard/DashboardHero.tsx b/src/components/dashboard/DashboardHero.tsx
index 25aa234..8d94ea1 100644
--- a/src/components/dashboard/DashboardHero.tsx
+++ b/src/components/dashboard/DashboardHero.tsx
@@ -1,3 +1,4 @@
+import { MASTERY_THRESHOLD } from "@/lib/constants";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
@@ -30,7 +31,7 @@ function getCTA(examId: string, overall: OverallProgress, domains: DomainProgres
}
// Exam ready - 85%+ mastery
- if (overall.masteryScore >= 85) {
+ if (overall.masteryScore >= MASTERY_THRESHOLD) {
return {
text: "Review Your Progress",
href: `/${examId}/progress`,
diff --git a/src/components/progress/ReadinessCard.tsx b/src/components/progress/ReadinessCard.tsx
index de85f61..951d8f9 100644
--- a/src/components/progress/ReadinessCard.tsx
+++ b/src/components/progress/ReadinessCard.tsx
@@ -5,6 +5,7 @@ import { ChevronDown, ChevronRight, Info } from 'lucide-react';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { ProgressIndicator } from '@/components/ui/progress-indicator';
import type { ReadinessEstimate, DomainReadiness } from '@/lib/progress/calculator';
+import { MASTERY_THRESHOLD, APPROACHING_THRESHOLD } from '@/lib/constants';
import { clsx } from 'clsx';
interface ReadinessCardProps {
@@ -116,7 +117,7 @@ export function ReadinessCard({ estimate, examId }: ReadinessCardProps) {
function DomainBar({ domain }: { domain: DomainReadiness }) {
const [expanded, setExpanded] = useState(false);
const masteryRounded = Math.round(domain.mastery);
- const colorClass = masteryRounded >= 85 ? 'text-green-600' : masteryRounded >= 60 ? 'text-amber-600' : 'text-red-600';
+ const colorClass = masteryRounded >= MASTERY_THRESHOLD ? 'text-green-600' : masteryRounded >= APPROACHING_THRESHOLD ? 'text-amber-600' : 'text-red-600';
const hasWeakTopics = domain.weakTopics.length > 0;
return (
diff --git a/src/components/ui/progress-indicator.tsx b/src/components/ui/progress-indicator.tsx
index 857104a..4885928 100644
--- a/src/components/ui/progress-indicator.tsx
+++ b/src/components/ui/progress-indicator.tsx
@@ -1,5 +1,6 @@
"use client"
+import { MASTERY_THRESHOLD, APPROACHING_THRESHOLD } from '@/lib/constants';
import { cn } from '@/lib/utils';
import * as ProgressPrimitive from "@radix-ui/react-progress";
@@ -16,8 +17,8 @@ interface ProgressIndicatorProps {
*/
export function ProgressIndicator({ value, className }: ProgressIndicatorProps) {
const indicatorColorClass =
- value >= 85 ? 'bg-green-600' :
- value >= 60 ? 'bg-amber-500' :
+ value >= MASTERY_THRESHOLD ? 'bg-green-600' :
+ value >= APPROACHING_THRESHOLD ? 'bg-amber-500' :
'bg-red-600';
return (
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
new file mode 100644
index 0000000..f951be1
--- /dev/null
+++ b/src/lib/constants.ts
@@ -0,0 +1,14 @@
+/** Score at or above which a topic is considered "mastered" / "exam ready" */
+export const MASTERY_THRESHOLD = 85;
+
+/** Score at or above which a topic is considered "developing" */
+export const APPROACHING_THRESHOLD = 60;
+
+/** Score at or above which readiness level is "approaching" (vs "building") */
+export const READINESS_APPROACHING_THRESHOLD = 65;
+
+/** Score at or above which dashboard tips switch to exam-prep category */
+export const EXAM_PREP_THRESHOLD = 75;
+
+/** Decimal equivalent for SQL queries (mastery_level is 0-1 scale) */
+export const MASTERY_THRESHOLD_DECIMAL = MASTERY_THRESHOLD / 100;
diff --git a/src/lib/content/sidebar.ts b/src/lib/content/sidebar.ts
index 0d5b230..6c7d600 100644
--- a/src/lib/content/sidebar.ts
+++ b/src/lib/content/sidebar.ts
@@ -1,3 +1,4 @@
+import { MASTERY_THRESHOLD } from '@/lib/constants';
import { getAllDomains } from './loader';
import { parseTopicSections } from './parser';
import { getAllTopicWindowedMasteries } from '@/lib/progress/mastery';
@@ -111,7 +112,7 @@ export function getSidebarHierarchyWithProgress(examId: string): SidebarHierarch
if (topicResult && topicResult.attempts > 0) {
topicMasterySum += masteryScore;
topicsWithProgress++;
- if (masteryScore >= 85) topicsCompleted++;
+ if (masteryScore >= MASTERY_THRESHOLD) topicsCompleted++;
}
return {
diff --git a/src/lib/dashboard/tips.ts b/src/lib/dashboard/tips.ts
index 10a31ab..43dafc0 100644
--- a/src/lib/dashboard/tips.ts
+++ b/src/lib/dashboard/tips.ts
@@ -1,3 +1,5 @@
+import { EXAM_PREP_THRESHOLD } from '@/lib/constants';
+
export interface Tip {
id: string;
category: "onboarding" | "assessment" | "study" | "feature" | "exam-prep";
@@ -136,7 +138,7 @@ function getTipCategory(state: UserState): Tip["category"] {
return "onboarding";
}
- if (state.masteryScore >= 75) {
+ if (state.masteryScore >= EXAM_PREP_THRESHOLD) {
return "exam-prep";
}
diff --git a/src/lib/llm/tool-handlers.ts b/src/lib/llm/tool-handlers.ts
index 13d0fc5..29a8e96 100644
--- a/src/lib/llm/tool-handlers.ts
+++ b/src/lib/llm/tool-handlers.ts
@@ -1,3 +1,4 @@
+import { MASTERY_THRESHOLD } from '@/lib/constants';
import { getTutorProgressContext } from '@/lib/progress/tutor-context';
import { getAllDomains, getTopicById, getTopicQuestions } from '@/lib/content/loader';
import { getLabMeta } from '@/lib/content/experiments';
@@ -466,7 +467,7 @@ export function handleSuggestNextStudyTopic(_params: Record, ex
}
// Skip mastered topics
- if (mastery >= 85) continue;
+ if (mastery >= MASTERY_THRESHOLD) continue;
const finalScore = Math.round(baseScore * weightMultiplier);
diff --git a/src/lib/progress/calculator.ts b/src/lib/progress/calculator.ts
index 0962012..18f1799 100644
--- a/src/lib/progress/calculator.ts
+++ b/src/lib/progress/calculator.ts
@@ -1,6 +1,7 @@
import { cache } from "react";
import { db } from "@/lib/db/client";
import { getAllDomains, getTopicById } from "@/lib/content/loader";
+import { MASTERY_THRESHOLD, MASTERY_THRESHOLD_DECIMAL, READINESS_APPROACHING_THRESHOLD } from "@/lib/constants";
import { getAllTopicWindowedMasteries } from './mastery';
import type { TopicMasteryResult } from './mastery';
@@ -132,7 +133,7 @@ export const getDomainProgress = cache((examId: string, domainId: string): Domai
const topicStats = db.prepare(`
SELECT
COUNT(*) as topics_with_progress,
- SUM(CASE WHEN mastery_level >= 0.85 THEN 1 ELSE 0 END) as completed_topics
+ SUM(CASE WHEN mastery_level >= ${MASTERY_THRESHOLD_DECIMAL} THEN 1 ELSE 0 END) as completed_topics
FROM topic_progress
WHERE exam_id = ? AND domain_id = ?
`).get(examId, domainId) as { topics_with_progress: number; completed_topics: number };
@@ -336,7 +337,7 @@ export const getReadinessEstimate = cache((examId: string): ReadinessEstimate =>
}
// Topics below 85% are weak (including unstudied ones)
- if (mastery < 85) {
+ if (mastery < MASTERY_THRESHOLD) {
weakTopics.push({
topicId: topic.meta.id,
topicName: topic.meta.shortName,
@@ -378,9 +379,9 @@ export const getReadinessEstimate = cache((examId: string): ReadinessEstimate =>
// Determine level
let level: 'ready' | 'approaching' | 'building';
- if (overallMastery >= 85) {
+ if (overallMastery >= MASTERY_THRESHOLD) {
level = 'ready';
- } else if (overallMastery >= 65) {
+ } else if (overallMastery >= READINESS_APPROACHING_THRESHOLD) {
level = 'approaching';
} else {
level = 'building';
@@ -413,7 +414,7 @@ function getAllDomainProgressBatch(examId: string): DomainProgress[] {
SELECT
domain_id,
COUNT(*) as topics_with_progress,
- SUM(CASE WHEN mastery_level >= 0.85 THEN 1 ELSE 0 END) as completed_topics
+ SUM(CASE WHEN mastery_level >= ${MASTERY_THRESHOLD_DECIMAL} THEN 1 ELSE 0 END) as completed_topics
FROM topic_progress
WHERE exam_id = ?
GROUP BY domain_id
diff --git a/src/lib/utils/colors.ts b/src/lib/utils/colors.ts
index 57569d2..80616dc 100644
--- a/src/lib/utils/colors.ts
+++ b/src/lib/utils/colors.ts
@@ -3,6 +3,7 @@
* Tailwind cannot handle dynamic class names (e.g., `border-${color}-500`),
* so we map domain color strings to hex values for use with style={{ }}.
*/
+import { MASTERY_THRESHOLD, APPROACHING_THRESHOLD } from '@/lib/constants';
const DOMAIN_COLOR_HEX: Record = {
blue: '#3b82f6',
@@ -25,8 +26,8 @@ export function getDomainColorHex(color: string): string {
* Uses Tailwind classes since these are static (known at build time).
*/
export function getMasteryDotColorClass(score: number): string {
- if (score >= 85) return 'bg-green-500';
- if (score >= 60) return 'bg-amber-500';
+ if (score >= MASTERY_THRESHOLD) return 'bg-green-500';
+ if (score >= APPROACHING_THRESHOLD) return 'bg-amber-500';
if (score > 0) return 'bg-red-500';
return 'bg-muted-foreground/30';
}
diff --git a/src/lib/utils/mastery.ts b/src/lib/utils/mastery.ts
index 98f2c23..682912c 100644
--- a/src/lib/utils/mastery.ts
+++ b/src/lib/utils/mastery.ts
@@ -2,13 +2,14 @@
* Centralized mastery utilities for consistent display across the application.
* Consolidates mastery scoring, labeling, and formatting functions.
*/
+import { MASTERY_THRESHOLD, APPROACHING_THRESHOLD } from '@/lib/constants';
/**
* Get mastery score color class based on threshold
*/
export function getMasteryColorClass(score: number): string {
- if (score >= 85) return "text-green-600";
- if (score >= 60) return "text-amber-600";
+ if (score >= MASTERY_THRESHOLD) return "text-green-600";
+ if (score >= APPROACHING_THRESHOLD) return "text-amber-600";
if (score > 0) return "text-red-600";
return "text-muted-foreground";
}
@@ -17,8 +18,8 @@ export function getMasteryColorClass(score: number): string {
* Get mastery label based on score
*/
export function getMasteryLabel(score: number): string {
- if (score >= 85) return "Mastered";
- if (score >= 60) return "Developing";
+ if (score >= MASTERY_THRESHOLD) return "Mastered";
+ if (score >= APPROACHING_THRESHOLD) return "Developing";
if (score > 0) return "In Progress";
return "Not Started";
}
@@ -30,8 +31,8 @@ export function getMasteryStatus(score: number): {
label: string;
variant: "default" | "secondary" | "destructive" | "outline";
} {
- if (score >= 85) return { label: "Exam Ready", variant: "default" };
- if (score >= 60) return { label: "Developing", variant: "secondary" };
+ if (score >= MASTERY_THRESHOLD) return { label: "Exam Ready", variant: "default" };
+ if (score >= APPROACHING_THRESHOLD) return { label: "Developing", variant: "secondary" };
if (score > 0) return { label: "Building Foundation", variant: "outline" };
return { label: "Not Started", variant: "outline" };
}