schoolos-migration-guard
> متى تُستعمل: "Django migration safety: expand/contract, NOT NULL, indexes, RunPython. Trigger: migration, makemigrations, schema change. هجرة، تعديل models.py"
SKILL.md.claude/skills/schoolos-migration-guard/SKILL.md
GitHubمتى تُستعمل: "Django migration safety: expand/contract, NOT NULL, indexes, RunPython. Trigger: migration, makemigrations, schema change. هجرة، تعديل models.py"
حارس هجرات SchoolOS
الغرض: أن تمرّ كلُّ هجرةٍ على قاعدةٍ حيّةٍ بلا توقّفٍ ولا فقدٍ ولا كسرٍ للنسخة المنشورة، وأن تعبر بوّابةَ CI من أوّل مرّة.
السببُ الحيّ: 2026-09-11 حذفت هجرةٌ عموداً (spread_days_scope) فسقطت ثماني صفحاتٍ في كلّ شجرةٍ أخرى ساعات (المصدر: CLAUDE.md، قسم «الهجرات: توسيعٌ ثمّ تقليص»).
متى تُستعمل ومتى لا
- نعم: أيُّ ملفٍّ جديدٍ أو معدَّلٍ تحت
*/migrations/، وأيُّ تعديلٍ في نموذج، ومراجعةُ طلبٍ فيه هجرة. - لا: هجراتُ
roadmap/migrations/— لجلسة «خارطة التجويد» وحدها، وغيرُها يُبلغها برمز البند ورقم الطلب (المصدر:CLAUDE.md، قسم الخارطة الحيّة). ولا قرارُ «هل يُشفَّر الحقل» (pdppl-pii-audit).
الإجراء
- حدِّد الخطوة قبل الكتابة: توسيعٌ (أضِف وانقل القراءة) أم تقليصٌ (احذف ما لم يعد يُقرأ) — لا الاثنان في إصدارٍ واحد. التفصيل:
references/00-policy-and-ci.md. - ولّد الهجرة في شجرتك ثمّ قارن رقمَها بآخر هجرةٍ للتطبيق على
origin/main(git fetchثمّgit ls-tree --name-only origin/main <app>/migrations/) — هجرتان من جلستين على الأصل نفسِه تُسقطان migrate. انظرreferences/03-parallel-sessions.md. - شغّل الحارس داخل حاوية جلستك (بادئةُ
$DCمعرَّفةٌ فيreferences/03-parallel-sessions.md):$DC python .claude/skills/schoolos-migration-guard/scripts/check_migration.py --file <app>/migrations/<file>.py --sqlيحلّل الملفَّ، ويولّد SQL بـsqlmigrateويصنّفه، ثمّ يمرّره على محلّلdjango-migration-linterنفسِه الذي تستدعيه البوّابة — فسطرُ «[مدقّق CI] خطأ» يعني أنّ CI سيسقط. رمزُ الخروج 1 عند أيّ 🔴. - عالج كلَّ 🔴 بالبديل المذكور في
references/01-operations-catalog.md، وراجع كلَّ 🟠 بحجم الجدول. - طبّقها على قاعدة جلستك واختبر:
$DC python manage.py migrateثمّ$DC python manage.py makemigrations --check --dry-run(بوّابةٌ في CI أيضاً) ثمّ الاختبارات بأمرCLAUDE.md. - في وصف الطلب: أيُّ خطوةٍ هذه (توسيع/تقليص)، وما يعكسها، وأنّ الحارس خرج 0. ثمّ الدفعُ والتحويل وفق schoolos-flow.
القواعد وأسبابها
- الهدّامُ خطوةٌ ثانيةٌ في إصدارٍ لاحق. حذفُ عمودٍ أو جدولٍ أو إعادةُ تسميتهما أو NOT NULL بلا افتراض يُسقط الشيفرةَ التي ما زالت تعمل: الهجراتُ تجري في مرحلة
preDeployوالنسخةُ القديمة وعاملُ Celery يخدمان (المصدر:scripts/railway-predeploy.sh،.railway/railway.ts)، وكلُّ شجرةٍ أخرى على شيفرتها. البوّابةُ تسقطه ولو كان صحيحاً منطقيّاً، والاستثناءُ قرارٌ في مراجعة الطلب (المصدر:CLAUDE.md). - عمودٌ NOT NULL جديد =
default=وdb_default=معاً. بـdefault=وحده يضيف Django العمودَ بافتراضٍ ثمّ يُسقطه (DROP DEFAULT) فتسقط كتابةُ النسخة القديمة، والبوّابةُ تُسقطه (NOT_NULL) — مُثبَتٌ بـsqlmigrateوlintmigrationsفي مختبرٍ على PostgreSQL 18. السابقة:operations/migrations/0053_excuse_review.py. - الفهرسُ على جدولٍ قائمٍ كبير =
AddIndexConcurrentlyوatomic = False؛ والقيدُ الفريدُ وتغييرُ نوع العمود على جدولٍ قائم تُسقطهما البوّابة (ADD_UNIQUE، ALTER_COLUMN). السابقة:operations/migrations/0055_attendance_exit_partial_index.py. - RunPython يرى نموذجاً تاريخيّاً: بلا
save()ولا خصائص ولا مديراتٍ مخصّصة (.live()…) — مُثبَتٌ في المختبر. فما يملؤهsave()(ثلاثيّةُnational_id/phone: خامٌ و_encryptedو_hmac) يُحسب صراحةً بـencrypt_field/hmac_field. السابقة:core/migrations/0020_populate_national_id_hmac_encrypted.py. التفصيل:references/02-data-migrations.md. - كلُّ RunPython/RunSQL له عكس (دالّةٌ أو
noopصريحٌ بسببه). البوّابةُ تحذّر فقط؛ المهارةُ أشدّ لأنّ فشلَ النشر بلا عكسٍ إصلاحٌ يدويٌّ على الإنتاج. - لا تعدّل هجرةً طُبّقت على أيّ قاعدة (جلستك أو المعاينة): Django لا يعيد تطبيقَ ما سُجّل، فتبقى القاعدةُ على المسوّدة صامتةً (المصدر: ذاكرة
feedback_local_roadmap_db_drift.md). أنشئ هجرةً جديدة، أو ارجع بها (migrate <app> <السابقة>) ثمّ عدّل. - ما يزيد عن 300ms لا يُحشر في هجرة: تعبئةُ جدولٍ كبير أمرُ إدارةٍ أو مهمّةٌ خلفيّة بعد النشر (المصدر: «القرارات الهندسيّة» في
~/.claude/CLAUDE.md).
فخاخٌ حقيقيّة
- خطأ:
AddField(..., field=models.TextField(default=""))لعمودٍ إلزاميّ. الصواب:models.TextField(default="", db_default=""). - خطأ:
RemoveFieldفي الطلب نفسِه الذي أوقف قراءةَ الحقل. الصواب: طلبٌ يوقف القراءةَ والكتابة ويُنشر، ثمّ طلبٌ لاحقٌ بالحذف (السابقة: حذفُStaffEvaluationبعد #417،shschool/settings/testing.py). - خطأ:
RunPython(forward)وحده. الصواب:RunPython(forward, backward)أوRunPython(forward, migrations.RunPython.noop)مع تعليقٍ يشرح لماذا لا عكس. - خطأ: في RunPython:
User.objects.filter(...).update(phone=new). الصواب: دفعاتٌ تحسبphone_encryptedوphone_hmacبالدالّتين ثمّbulk_updateبالأعمدة الثلاثة. - خطأ:
AddIndexعلىstudentattendance. الصواب:AddIndexConcurrentlyفي هجرةٍ وحدَها بـatomic = False. - خطأ:
default=uuid.uuid4معunique=TrueفيAddField. الصواب:null=Trueأوّلاً، ثمّ تعبئةٌ لكلّ صفّ، ثمّ القيد — الدالّةُ تُحسب مرّةً واحدةً لكلّ الصفوف القائمة (مُثبَتٌ بـsqlmigrate). - خطأ: تعديلُ
0056_x.pyبعد تطبيقها على قاعدة جلستك «لأنّها لم تُدفع بعد». الصواب: ارجع بها أوّلاً، أو هجرةٌ جديدة. - خطأ: «المسارُ الوحيد
D:\shschool_mvpولا تعدّل في worktrees» — أُلغيت 2026-09-08. الصواب: اعمل في شجرتك على قاعدتها (CLAUDE.md، القاعدة رقم 1).
المراجع
| الملف | متى تقرأه |
|---|---|
references/00-policy-and-ci.md |
قبل أيّ هجرةٍ هدّامة، أو حين تسقط وظيفةُ المدقّق في CI وتريد معرفةَ ما تفحصه بالضبط |
references/01-operations-catalog.md |
لكلّ عمليّةٍ في ملفّك: خطرُها، وSQL الذي تولّده، وحكمُ البوّابة، والبديل |
references/02-data-migrations.md |
حين تكتب RunPython/RunSQL، ولا سيّما على حقولٍ مشفّرة أو AuditLog أو جدولٍ كبير |
references/03-parallel-sessions.md |
لتشغيل الأوامر في حاوية جلستك، أو عند تعارض ترقيم، أو قاعدةٍ متأخّرةٍ عن main |
references/99-test-cases.md |
عند تعديل الوصف أو الجسم — حالاتُ التفعيل واختباراتُ المخرج |