محرك تخطيط قرآني متكامل، يُولّد خططاً يومية للحفظ والمراجعة، ويتابع تقدم الطلاب جلسةً بجلسة.
- نظرة عامة
- بنية المشروع
- البدء السريع
- نظام التخطيط — Planning Engine
- QuranRepository — طبقة البيانات
- نظام الأخطاء — Error System
- سيناريوهات الاستخدام
- التصدير
- الاختبارات
- القرارات الهندسية
مكين مكتبة TypeScript تُوفّر:
| الوظيفة | الوصف |
|---|---|
| توليد الخطة | محاكاة يومية لمسارات الحفظ والمراجعة الصغرى والكبرى |
| استعلامات القرآن | تحويل السورة:الآية إلى index والعكس، حساب الأسطر بين موقعين |
| تصدير الخطة | طباعة في الكونسول + تصدير Excel |
الاتجاهان مدعومان:
isReverse: false— من الفاتحة إلى الناس (الحفظ التقليدي)isReverse: true— من الناس إلى الفاتحة
src/
├── main.ts # نقطة الدخول
│
├── builders/
│ ├── PlanBuilder.ts # Fluent API لإنشاء الخطة
│ ├── HifzSystem.ts # Subsystem: إنشاء وتوصيل مسارات الحفظ
│ └── PlanTypes.ts # Type contracts (ScheduleConfig, LocationConfig…)
│
├── core/
│ ├── TrackManager.ts # محرك المحاكاة اليومية
│ ├── QuranRepository.ts # Singleton: كل استعلامات بيانات القرآن
│ ├── PlanContext.ts # سياق كل يوم محاكاة (يُمرَّر للاستراتيجيات)
│ ├── constants.ts # Enums: TrackId, WindowMode
│ └── types.ts # PlanDay, PlanEvent
│
├── data/
│ ├── QuranStaticData.ts # مصفوفات البيانات الثابتة (O(1) lookups)
│ ├── create_quranStaticData.py # سكربت يولد البيانات من CSV
│ └── ... # ملفات CSV (قاعدة البيانات الأولية)
│
├── tracks/
│ ├── BaseTrack.ts # Abstract: state machine + commitStep
│ ├── LinearTrack.ts # مسار خطي (حفظ جديد وغيره)
│ ├── WindowTrack.ts # مسار نافذة متحركة (مراجعة صغرى)
│ └── LoopingTrack.ts # مسار دوري (مراجعة كبرى)
│
├── strategies/
│ ├── IMovementStrategy.ts # Interface
│ ├── LinearStrategy.ts # تقدم خطي بمقدار ثابت
│ ├── WindowStrategy.ts # نافذة تتبع مسار آخر
│ └── LoopingStrategy.ts # دوران مع وعي بحاجز (Wall)
│
├── constraints/
│ ├── ConstraintManager.ts # يجمع ويطبق القيود
│ └── WallConstraint.ts # يمنع مسار من تجاوز مسار آخر
│
├── errors/
│ ├── PlanError.ts # Custom Error class
│ ├── PlanErrorCode.ts # Enum: رموز الأخطاء
│ ├── Severity.ts # Enum: ERROR / WARNING / INFO
│ └── index.ts # Barrel export
│
├── utils/
│ ├── Algorithms.ts # findExponentialStopIndex (Prefix Sum search)
│ ├── DateUtils.ts # أيام العمل، إضافة أيام
│ └── PlanExporter.ts # طباعة + تصدير Excel
│
└── tests/
└── planErrors.test.ts # 12 اختبار: أخطاء + سلوك الإصلاحات
npm install
npm startimport { PlanBuilder } from './builders/PlanBuilder';
import { WindowMode } from './core/constants';
const manager = new PlanBuilder()
.setSchedule({
startDate: '2026-02-01',
daysPerWeek: 5, // الأحد - الخميس
limitDays: 30, // 0 = استمر حتى التوقف التلقائي
isReverse: false // الفاتحة → الناس
})
.addHifz(
7.5, // 7.5 سطر يومياً (15 سطر = وجه)
{ surah: 1, ayah: 1 }, // البداية: الفاتحة
{ surah: 2, ayah: 286 } // اختياري: النهاية عند البقرة
)
.addMinorReview(3, WindowMode.GRADUAL) // مراجعة آخر 3 دروس
.addMajorReview(
15 * 20, // 20 وجه يومياً
{ surah: 1, ayah: 1 } // اختياري: نقطة البداية
)
.stopWhenCompleted() // توقف عند انتهاء الحفظ
.build();
const plan = manager.generatePlan();PlanBuilder يُوفّر Fluent API آمن النوع لبناء الخطة. يعمل على مبدأ Deferred Execution:
يحتفظ بطلبات TrackRequest ويُنفّذها بترتيب صحيح عند .build().
PlanBuilder
.setSchedule() ← إلزامي
.addHifz() ← إلزامي في HIFZ_ECOSYSTEM
.addMinorReview() ← اختياري
.addMajorReview() ← اختياري
.stopWhenCompleted() ← اختياري
.build() → TrackManager
حماية MODE_MIXING: لا يمكن مزج addHifz() مع مسارات من نمط آخر (مستقبلاً: WERD_ECOSYSTEM).
يُرمى PlanError(MODE_MIXING) في حال المحاولة.
| الخاصية | النوع | الوصف |
|---|---|---|
startDate |
string |
تاريخ البداية (ISO) |
daysPerWeek |
number |
عدد أيام الدراسة في الأسبوع (1-7) |
limitDays |
number? |
حد أقصى للأيام (0 = لا حد) |
endDate |
string? |
تاريخ انتهاء ثابت |
isReverse |
boolean? |
اتجاه الحفظ |
كل مسار يرث من BaseTrack ويُطبّق نمط State Machine:
| المسار | الاستراتيجية | الاستخدام |
|---|---|---|
LinearTrack |
LinearStrategy |
أي تقدم خطي بمقدار ثابت (الحفظ الجديد وسواه) |
WindowTrack |
WindowStrategy |
مراجعة نافذة متحركة تتبع مسار آخر |
LoopingTrack |
LoopingStrategy |
مراجعة دورية مع وعي بالحاجز |
ملاحظة تصميمية: أسماء المسارات وصفية للسلوك — وليست حكراً على استخدامها الحالي.
LinearTrack← أي تقدم خطي ثابت، ليس الحفظ حصراًWindowTrack← أي مسار يتبع نافذة من تاريخ مسار آخرLoopingTrack← أي مسار دوري يحترم حاجزاًالمشروع مصمم ليقبل مسارات جديدة بأفكار مختلفة — يكفي تطبيق
IMovementStrategyوتمديدBaseTrack.
دورة حياة المسار:
calculateNextStep(context) → StepResult | null
↓
commitStep(step, date) → يُحدّث state + history
StepResult flags:
'completed'— المسار وصل نهايته،currentIdxيبقى عندendIdx(لا يتجاوز الحدود)'reset'— المسار الدوري عاد للبداية بعد الوصول للحاجز أو النهاية
كل استراتيجية تُطبّق IMovementStrategy وتُجيب على سؤال واحد:
"ما هي الخطوة التالية لهذا المسار في هذا اليوم؟"
- السلوك: يتقدم بمقدار ثابت (
amount) كل يوم - الحد الاختياري (
endIdx): إذا وصل الـendIdx، يُعلمcompleted - البحث:
findExponentialStopIndex— O(log k) على Prefix Sum Array - مُستخدم في:
LinearTrack→ حفظ جديد بحد نهاية اختياري
- السلوك: نافذة تتبع مسار آخر (عادةً HIFZ)، تُظهر آخر N درس
- وضعان:
| الوضع | WindowMode.GRADUAL |
WindowMode.FIXED |
|---|---|---|
| المقصد | طالب يبدأ من الصفر | طالب لديه محفوظات سابقة |
| اليوم 1 | لا مراجعة (لا تاريخ) | N درس كامل من اليوم الأول |
| الحساب | يقرأ التاريخ ويبني النافذة تدريجياً | يحسب للخلف من موقع الحفظ الحالي |
- السلوك: يتقدم بمقدار ثابت ويعود للبداية عند الوصول إلى الحاجز أو نهاية القرآن
- الحاجز (Wall): موقع مسار آخر (الصغرى أو الحفظ) — لا يتجاوزه
- الصمت: إذا كان لا حراك ممكن، يُرجع
null(المسار ينتظر) - مُستخدم في:
LoopingTrack→ مراجعة كبرى
يمنع مسار (tracking) من تجاوز مسار آخر (target).
new WallConstraint(
trackingId, // المسار الذي سيتأثر بالقيد
targetId, // المسار الذي يُشكّل الحاجز
useHistory, // true = استخدم التاريخ، false = استخدم currentIdx مباشرة
safetyMarginDays, // هامش أمان (عدد الدروس)
fallbackTargetId? // بديل في مرحلة الـ Bootstrap (قبل أن يكون للهدف تاريخ)
)الاستخدام الافتراضي (HifzSystem):
- الكبرى لا تتجاوز الصغرى (أو الحفظ إذا لم توجد الصغرى)
يُشغّل الخطة يوماً بيوم:
generatePlan()
↓
لكل يوم عمل:
↓
[sort tracks by id: HIFZ(1) → MINOR(2) → MAJOR(3)]
↓
لكل مسار:
calculateNextStep(dayContext) → StepResult?
commitStep(step, date)
→ إنشاء PlanEvent
↓
فحص StopCondition
↓
تقدم التاريخ (تخطي الإجازات)
الترتيب مفروض بالـ id: ضمان أن WindowStrategy تقرأ تاريخ الحفظ قبل أن تحتاج إليه.
شروط التوقف:
limitDaysوصل حدهendDateتجاوزstopWhenCompleted()+ اكتمال مسار الحفظ- حارس داخلي: 5000 يوم كحد أقصى مطلق
Singleton — مثيل واحد مشترك بين كل المكونات (~150KB في الذاكرة).
const repo = QuranRepository.getInstance();| الدالة | التعقيد | الوصف |
|---|---|---|
getIndexFromLocation(surah, ayah, isReverse) |
O(1) | سورة:آية → index في المصفوفة التراكمية |
getLocationFromIndex(index, indexMap) |
O(1) | index → سورة:آية |
moveLocation(current, linesToAdd, isReverse) |
O(log k) | تحريك موقع بعدد أسطر |
getLinesBetween(from, to, direction) |
O(1) | عدد الأسطر بين موقعين |
getSurahName(surahNum) |
O(1) | اسم السورة |
getAyahCount(surahNum) |
O(1) | عدد الآيات في السورة |
const lines = repo.getLinesBetween(
{ surah: 2, ayah: 1 }, // من
{ surah: 2, ayah: 286 }, // إلى (شامل)
'auto' // direction (الافتراضي: 'auto' لتحديد الاتجاه تلقائياً)
);
// → 712.5 سطر
// مثال عكسي:
const rev = repo.getLinesBetween(
{ surah: 114, ayah: 1 },
{ surah: 1, ayah: 7 },
true // ← يستخدم مصفوفة الاتجاه العكسي
);الخوارزمية:
lines = cumArray[endIdx] - cumArray[startIdx - 1]— عمليتا قراءة + طرح واحد. O(1) لا يُحسَّن أكثر.|abs|يضمن صحة النتيجة بصرف النظر عن ترتيب المدخلين. القيمة الافتراضية للاتجاه هي'auto'والتي تستنتج الاتجاه تلقائياً بناءً على الترتيب النسبي للموقعين.
جميع الأخطاء instanceof Error وتحمل بنية موحدة:
class PlanError extends Error {
code: PlanErrorCode; // رمز مُعرَّف
severity: Severity; // ERROR | WARNING | INFO
message: string; // رسالة بشرية
context: object; // بيانات إضافية للتشخيص
}| الرمز | النوع | المصدر | السلوك |
|---|---|---|---|
INVALID_LOCATION |
ERROR | QuranRepository |
throw |
MODE_MIXING |
ERROR | PlanBuilder |
throw |
MISSING_SCHEDULE |
ERROR | PlanBuilder |
throw |
START_AFTER_END |
ERROR | HifzSystem |
throw |
MAJOR_REVIEW_AHEAD |
ERROR | HifzSystem |
throw |
// لا يُرمى — يُطبع فقط
PlanError.warn(
PlanErrorCode.INVALID_LOCATION,
'موقع غير صالح.',
{ surah: 0, ayah: 0 }
);try {
const manager = new PlanBuilder()...build();
} catch (err) {
if (err instanceof PlanError) {
console.log(err.code); // 'MISSING_SCHEDULE'
console.log(err.severity); // 'ERROR'
console.log(err.context); // {}
}
}new PlanBuilder()
.setSchedule({ startDate: '2026-01-01', daysPerWeek: 5, isReverse: false })
.addHifz(7.5, { surah: 1, ayah: 1 })
.stopWhenCompleted()
.build();new PlanBuilder()
.setSchedule({ startDate: '2026-01-01', daysPerWeek: 5, limitDays: 0, isReverse: true })
.addHifz(10, { surah: 114, ayah: 1 }) // من الناس
.addMinorReview(5, WindowMode.GRADUAL) // آخر 5 دروس
.addMajorReview(300, { surah: 67, ayah: 1 }) // من الملك
.stopWhenCompleted()
.build();.addHifz(
7.5,
{ surah: 2, ayah: 1 }, // البداية
{ surah: 2, ayah: 286 } // النهاية: داخل البقرة فقط
).addMinorReview(5, WindowMode.FIXED) // 5 دروس من اليوم الأولمدعوم — يمكن إضافة
addMajorReviewوحدها. ستعمل بدون Hifz track. في هذه الحالة لن يوجدWallConstraintمرتبط بالحفظ.
const repo = QuranRepository.getInstance();
// كم سطراً في جزء عم؟
const lines = repo.getLinesBetween(
{ surah: 78, ayah: 1 }, // النبأ
{ surah: 114, ayah: 6 } // الناس (يتم استنتاج الاتجاه تلقائياً)
);const exporter = new PlanExporter();
// طباعة في الكونسول
exporter.printToConsole(plan);
// تصدير Excel
await exporter.exportToExcel(plan, 'QuranPlan_2026.xlsx');npx ts-node src/tests/planErrors.test.ts12 اختبار — 5 suites:
| Suite | ما يختبره |
|---|---|
| 1 | PlanError: instanceof، الحقول، warn() |
| 2 | أخطاء Builder: MISSING_SCHEDULE، MODE_MIXING، START_AFTER_END، MAJOR_REVIEW_AHEAD |
| 3 | أخطاء البيانات: INVALID_LOCATION (3 حالات + context) |
| 5 | Fix 1 — currentIdx ≤ globalMax بعد اكتمال الحفظ |
| 6 | Fix 3 — WindowStrategy تقرأ تاريخ Hifz بشكل صحيح |
بدلاً من حساب الأسطر في كل استعلام، البيانات محضّرة مسبقاً كـ Float32Array تراكمية.
كل استعلام نطاق (بين موقعين) = عمليتا قراءة + طرح → O(1) مضمون.
QuranRepository مثيل واحد مشترك (~150KB). يُمرَّر بـ Dependency Injection للمكونات التي تحتاجه — لا getInstance() مبعثرة في كل مكان.
new TrackManager(config, QuranRepository.getInstance())يُسهّل الاختبار (mock repository) ويكسر التبعيات الدائرية.
[...tracks.values()].sort((a, b) => a.id - b.id)
// HIFZ(1) → MINOR_REVIEW(2) → MAJOR_REVIEW(3)الـ WindowStrategy تعتمد على تاريخ الحفظ — الترتيب ضرورة منطقية، لا افتراض ضمني.
PlanBuilder لا يُنشئ المسارات فوراً — يحتفظ بقائمة TrackRequest ويُرتّبها بـ priorityMap عند .build(). يضمن بناء صحيح بصرف النظر عن ترتيب استدعاءات .addHifz() / .addMinorReview().
عند اكتمال مسار ('completed' flag): currentIdx = endIdx (آخر index صالح).
WallConstraint يقرأ currentIdx مباشرة — قيمة endIdx + 1 ستُعطي undefined على Float32Array.