بين يدي فريقك playbook الـ mechanical-migration — الوصفة المجرّبة لمسح الـ blast radius، وتحويل الحالات المباشرة على مثال منجَز يدويًا، والتأشير على الصعبة، وتشغيل الحزمة لإثبات أن شيئًا لم ينكسر. هذه الوحدة طبقة أعلى من الوصفة: هنا تُتقن الحُكم الذي يجعل الـ migration جديرة بالثقة — تفكير الـ blast radius الذي يقرر النطاق قبل مسّ ملف واحد، وفصل الآلي عن المستلزِم للحُكم الذي يمنع تشويه الحالات الصعبة في صمت، والقرار الموثَّق الذي يحوّل TODO مؤشَّرًا عليه إلى شيء يستطيع المهندس التالي البناء عليه.
«قائمة “لم أستطع تحويله آليًا” هي أهم النواتج — اقرأها بعناية. هناك بالضبط تختبئ القرارات الحقيقية والأخطاء الخفية.» هذه هي العبارة التي يدقّ عليها الـ playbook. والإتقان هو الحُكم الذي يحوّل تلك القائمة من كومة تعليقات TODO إلى مجموعة قرارات موثَّقة: ما المشكلة، وما الخيارات، وماذا اخترت ولماذا.
مسح الـ blast radius — قبل مسّ ملف واحد
الـ playbook يقول: امسح كل استخدام قبل تحويل أي شيء. الإتقان هو الانضباط الذي يجعل المسح عملًا حقيقيًا لا شعيرة.
- صنّف بتعقيد التحويل، لا بالملف. المسح الذي يعود بـ«82 ملفًا فيها استخدامات» لا ينفع. أما المسح الذي يصنّف — «62 نداء
.format()مباشرًا يُحوَّل واحدًا لواحد، و9 تستعمل.utcOffset()الذي لا مكافئ مباشرًا له، و3 تسلسل عمليات بنمط لا يدعمه date-fns» — فهو ما تبني منه خطة فعلًا. التصنيف هو البصيرة؛ وعدد الملفات هو الضجيج. - جِد الاستخدامات الغريبة أولًا. أثمن ما في مسح الـ blast radius هو الشواذّ — الاستخدام الذي يفعل شيئًا غير معتاد، والنداء المتسلسل على غير المتوقع، والاستيراد الديناميكي لا الساكن. تلك هي الحالات التي تفشل في صمت لا بصوت إن أخطأتها الـ migration، وهي التي سيشوّهها المرور الآلي على الأرجح إن لم يعلم بوجودها.
- «لا تغيّر شيئًا بعد» هي التعليمة الحاملة. المسح والتحويل حركتان منفصلتان. المسح يقول لك ما الذي ينتظرك؛ والتحويل هو ما تفعله حياله. إجراؤهما معًا — أن تمسح وأنت تحوّل — يعني اتخاذ قرارات حالةً حالة دون الصورة الكاملة لما تحوّله. افصلهما، دائمًا.
- استعمل subagent للـ repos الكبيرة. في repo فيه مئات الوحدات، قد يزاحم المسحُ السياقَ الذي تحتاجه للتحويل. الـ playbook يذكر اللجوء إلى subagent للبحث الواسع — وهذا غير اختياري في الـ repo الكبير. المسح الذي يعايِن عيّنة من الـ repo ويسمّي نفسه كاملًا هو المسح الذي فاتته الاستخدامات الثلاثة غير المعتادة التي ستنكسر وقت التشغيل.
الأغلبية الآلية — حوِّل على المثال، وأشِّر على الباقي
الـ playbook يطلب تحويل الحالات المباشرة على مثال منجَز يدويًا وترك الصعبة بتعليق TODO. الإتقان هو الحُكم الذي يفصل الحالة المباشرة عن الصعبة — واليقظة التي تلتقط حالة صعبة حُوّلت في صمت كأنها مباشرة.
- المثال المنجَز يدويًا هو العقد. قبل أن يحوّل Claude ملفًا واحدًا، تريه مثالًا واحدًا حوّلته أنت تحويلًا صحيحًا. ذلك المثال هو الهدف — يطابق Claude ناتجه عليه في كل حالة يحكم أنها مباشرة. بلا المثال يخمّن Claude التحويل المقصود؛ ومعه يصير المرور الآلي قابلًا للتحقق على شيء ملموس اعتمدتَه أصلًا.
- «لا مكافئ نظيف» تعني اتركه كما هو. حين لا يكون للاستخدام مكافئ واحدًا لواحد في الهدف — دالة moment.js تستلزم تركيب ثلاث دوال من date-fns، أو خاصية لغات تعمل بشكل مختلف في الهدف، أو نمط إزاحة منطقة زمنية يحتاج حكمًا — الرد الصحيح تركه بالضبط كما هو والتأشير عليه. لا تقريبه، ولا تجربة الأقرب لعل الاختبارات تلتقط، ولا اختيار ما ينجح في الـ compile في صمت. المؤشَّر عليه غير المتغيّر هو الناتج الأمين؛ و«المُهاجَر» بفرق سلوك خفي هو الخطِر.
- الـ diff يفصل الآلي عن القرارات. بعد جولة التحويل، ينبغي أن يكون الـ diff قابلًا للتجهيز في مجموعتين واضحتين: التحويلات الآلية (التسعون المملة، همّ واحد) والحالات المقررة يدويًا (العشرة التي احتاجت حكمًا، همّ آخر). هذا الفصل هو ما يجعل الـ PR قابلًا للمراجعة — يقرأ المُراجِع المسح الآلي سريعًا ويركّز انتباهه على الحالات المقررة، حيث وقعت التعديلات الحقيقية.
- لا تدّعِ «هاجرتُ كل شيء» وهناك حالات مؤشَّر عليها. الـ migration التي تحوّل كل ملف ولا تؤشّر على شيء يحتاج قرارًا إما لم تكن فيها حالات صعبة (نادر) وإما شوّهت الحالات الصعبة (شائع). المرور الآلي لا يكتمل حين تفرغ المؤشرات؛ يكتمل حين تمثّل المؤشرات بأمانة كل حالة احتاجت حكمًا.
القرار الموثَّق — ما الذي يحوّل TODO إلى ناتج حقيقي
الـ playbook يذكر التأشير على الحالات الصعبة بتعليقات // TODO: migrate manually —. الإتقان هو الحُكم الذي يحوّل تلك المؤشرات إلى قرارات موثَّقة: لا «هذا يحتاج التفاتة» بل «هذه كانت المشكلة، وهذه الخيارات، وهذا ما قررته ولماذا».
- الـ TODO الشارح خير من الـ TODO الطالب. «// TODO: migrate manually — يستعمل .utcOffset() الذي لا مكافئ مباشرًا له في date-fns» مؤشرٌ. أما «// DECISION: أبقينا moment.js لهذا الاستخدام. .utcOffset() يقابله في date-fns
differenceInMinutes(toDate(d), new Date())لكن الحساب يختلف عند التوقيت الصيفي — تحققنا أن هذا السلوك مقصود (طوابع الفوترة دائمًا بتوقيت الخليج بلا إزاحة صيفية). استعملناformat(d, "yyyy-MM-dd'T'HH:mm:ss+04:00")بدلًا» فقرارٌ — يفهم مهندس المستقبل ما المشكلة، وما الذي دُرس، وما الذي اختير. - لكل قرار خيارات ومفاضلة. الـ playbook يقول: «لكل حالة مؤشَّر عليها، أرني الكود القديم، واشرح لماذا لا مكافئ نظيفًا، وأعطني أفضل خيارين بمفاضلتهما. وسأقرر كل واحدة.» الإتقان أن تمرّ بتلك الحلقة فعلًا لكل حالة — لا أن تُترك الحالات مؤشَّرًا عليها لـ sprint قادم. الوحدة تُقيَّم على هل للحالات الصعبة قرارات موثَّقة، لا مؤشرات فحسب.
- حُكمك هو المُدخل الذي يُغلق الحلقة. يعرض Claude الخيارات والمفاضلات؛ وأنت تحسم. وليس ذلك إجراءً شكليًا — إنه الحُكم الذي تختبره هذه الوحدة. القرار الموثَّق الذي يقول «اخترت الخيار (أ) لأن طوابع الفوترة دائمًا بتوقيت الخليج +4 بلا توقيت صيفي، فتقريب date-fns يُدخل فرقًا صامتًا لن نراه إلا مرتين في السنة عند تحويل ساعةٍ لا يحدث في الخليج أصلًا» يُظهر حُكمًا هندسيًا. أما «اخترت الخيار (أ)» فلا.
- سجل القرارات وثيقة دائمة. بعد هبوط الـ migration، تكون القرارات الموثَّقة هي المرجع لكل من يسأل «لماذا ما تزال هذه الحالة على moment.js؟» أو «لماذا تُحسب هذه المنطقة الزمنية بشكل مختلف؟» خلاصةُ وصفِ الـ PR تحمل المستوى العام؛ والتعليقات في الكود تحمل مستوى الحالة. وكلاهما دائم، لأن القرار يجب أن يبقى قابلًا للتتبع إلى الأبد.
الحزمة هي الحكم — أثبِت أن المرور الآلي لم يكسر شيئًا
الـ playbook يُختم بتشغيل الحزمة ومراجعة الـ diff الكامل. الإتقان هو الانضباط الذي يجعل ذلك الفحص الأخير أمينًا.
- شغّل الحزمة على جولة التحويل، لا في النهاية فقط. بعد هبوط التسعين الآلية، شغّل الحزمة قبل معالجة الحالات المقررة. هذا يفصل فشلًا سبّبه المرور الآلي عن فشل سبّبته حالة مقررة — ويجعل صحة كل فئة قابلة للتحقق على حدة.
- اقرأ الـ diff في مجموعتيه المنطقيتين. المسح الآلي والحالات المقررة نوعان مختلفان من التعديل يستلزمان نوعين مختلفين من المراجعة. والمُراجِع الذي يقرؤهما مختلطَين لا يستطيع أن يقول أي نوع أدخل أي مشكلة. جهّز المسح مجموعة commits والقرارات مجموعة أخرى، كي تعكس بنيةُ الـ diff البنيةَ المفهومية.
- الفشل في الحالات المقررة إشارة تُستقصى، لا تُصحَّح. إن احمرّت حالة مقررة، فالرد الصحيح أن تنظر لماذا — هل أدخل القرارُ الفشلَ، أم كان هناك اختبار خاطئ أصلًا، أم يشير الفشل إلى فرق سلوك لم يكن في الحسبان؟ لا تصلح اختبارًا أبدًا لتُنجح migration؛ استقصِ لماذا احمرّ.
تكليفك
أكمِل حلقة الـ migration كاملة لـتغيير حقيقي واحد على مستوى الـ repo — على الـ codebase الخاص بك (وهذا ما ننصح به) أو migration moment.js إلى date-fns في «ميزان» المعروضة في هذه الوحدة. افتح مجلد المشروع في Claude Desktop وفيه CLAUDE.md واعمل داخل المحادثة — وتشغيل الحزمة هو خطوة مسار الـ Power Track.
ناتج الوحدة الخامسة — migration على نطاق واسع
1. مسح الـ blast radius (صفحة واحدة)
- العدد الكلي للاستخدامات والتصنيف (آلي / صعب / غير معتاد)
- أغرب ثلاثة استخدامات مسمّاة بأعيانها ولماذا هي غير معتادة
- المنهج: تحويل الحالات الآلية على [مثالك المنجَز يدويًا]،
وترك الصعبة مؤشَّرًا عليها بـ TODO
2. التحويل الآلي (الـ diff، مجهَّزًا منفصلًا عن القرارات)
- همّ واحد: المسح الآلي
- الحزمة شُغّلت بعد المسح (قبل الحالات المقررة)
3. القرارات الموثَّقة (صفحة أو تعليقات في الكود)
- لكل حالة صعبة: ما المشكلة، والخيارات بمفاضلاتها، وما الذي
قررته ولماذا (لا «اخترت الخيار أ» — المنطق الفعلي)
- كل حالة مقررة commit مستقل عن المسح الآلي
4. نتائج الحزمة الختامية (خضراء، أو الأحمر مع تفسيره)
كيف يجري التقييم — المعايير
معايير الـ migration
1. مسح الـ blast radius مصنَّف
العدد الكلي للاستخدامات مصنَّفًا بالتعقيد (آلي / صعب / غير
معتاد). والحالات الصعبة وغير المعتادة مسمّاة بأعيانها قبل بدء
أي تحويل.
2. الصعبة مؤشَّر عليها، لا مشوَّهة في صمت
كل حالة بلا مكافئ نظيف تُركت كما هي وأُشّر عليها — لا قُرّبت
إلى ما ينجح في الـ compile ويتصرف بشكل مختلف.
3. القرارات موثَّقة بمنطقها
لكل حالة صعبة: ما المشكلة، والخيارات المدروسة، والقرار بمنطقه
الفعلي — لا «اخترنا الخيار أ» فحسب.
4. الـ diff في مجموعتين منطقيتين
المسح الآلي والحالات المقررة commits منفصلة (تُراجَع كلٌّ على
حدة). والحزمة شُغّلت بعد المسح، قبل القرارات.
5. الحزمة خضراء في الختام
الحزمة كاملة خضراء بعد كل التعديلات. وإن احمرّت: الاختبار
الفاشل مسمًّى، والاستقصاء معروض، ولا اختبار عُدّل لطمس إشارة
حقيقية.
المستوى المطلوب، معروضًا — نموذج إجابة («ميزان»)
(التحليل بالعربية؛ والكود والمسارات بالإنجليزية — وهذا هو النمط الثنائي الذي يعلّمه المسار.)
مسح الـ blast radius — migration moment.js ← date-fns في «ميزان»
الإجمالي: 74 استخدامًا عبر 23 ملفًا.
التصنيف:
- 62 مباشرًا: .format() و.add() و.subtract() و.isBefore()
و.isAfter() و.diff() — لكلها مكافئات date-fns نظيفة واحدًا
لواحد.
- 9 صعبة: .utcOffset() مستعمَلة لحسابات توقيت الخليج في
src/lib/billing.ts وsrc/lib/invoice-calc.ts — لا مكافئ مباشرًا؛
date-fns يتعامل مع إزاحات UTC بشكل مختلف عن نموذج moment.
- 3 غير معتادة: استيراد ديناميكي في src/scripts/backfill.ts
(moment يُستورَد داخل دالة async بحسب flag وقت التشغيل)،
واستخدام في ملف اختبار Playwright
(src/tests/e2e/invoices.spec.ts)، وآخر في ملف migration
(supabase/migrations/20260101_add_invoice_dates.sql — يذكر
moment في تعليق SQL، لا في منطق).
الأغرب: ملف الـ SQL — يظهر moment في تعليق فقط، آمنٌ تركُه
(التعليقات لا تُنفَّذ). والاستيراد الديناميكي في backfill.ts يستلزم
نقل الاستيراد إلى أعلى الملف بعد الـ migration.
المثال المنجَز يدويًا (العقد): moment().format('YYYY-MM-DD') ←
format(new Date(), 'yyyy-MM-dd') [ملاحظة: date-fns يستعمل 'y'
الصغيرة للسنة و'd' الصغيرة لليوم — يَسهُل الخطأ فيها].
قرار موثَّق — حالات utcOffset في billing.ts عند «ميزان»
الحالة الصعبة: src/lib/billing.ts:142–156
القديم: moment(invoiceDate).utcOffset('+04:00').format('YYYY-MM-DD')
المشكلة: لا utcOffset() في date-fns؛ والمكافئ إما حساب إزاحة يدوي
وإما مكتبة date-fns-tz.
الخيار أ: استعمال date-fns-tz (اعتمادية جديدة).
format(utcToZonedTime(invoiceDate, 'Asia/Dubai'), 'yyyy-MM-dd',
{ timeZone: 'Asia/Dubai' })
المفاضلة: صحيح للمناطق ذات التوقيت الصيفي، لكن توقيت الخليج بلا
توقيت صيفي، فهذه اعتمادية لا لزوم لها في حالتنا.
الخيار ب: حساب إزاحة UTC+4 يدوي.
const gst = new Date(invoiceDate.getTime() + 4 * 60 * 60 * 1000)
format(gst, 'yyyy-MM-dd')
المفاضلة: بسيط وصحيح لتوقيت الخليج (بلا توقيت صيفي)، لكنه سيفشل
في صمت إن خدمنا يومًا منطقة بتوقيت صيفي دون تحديث هذا الكود.
القرار: الخيار ب — حساب UTC+4 اليدوي.
المنطق: توقيت الخليج UTC+4 دائمًا، بلا إزاحة صيفية. وطوابع الفوترة
تُولَّد على الخادم في نسخة Supabase بمنطقة دبي. وإضافة date-fns-tz
لإزاحة ثابتة أبدًا تعقيدٌ لا لزوم له. أُضيف تعليق عند موضع النداء
يشرح لماذا هذا آمن وما الذي يجب أن يتغيّر إن أضفنا مناطق بتوقيت
صيفي.
ما الذي أثبتّه — وما التالي
باجتيازك هذه المعايير تكون قد أظهرت الانضباط الذي يجعل migration على مستوى الـ repo جديرة بالثقة لا مرجوّة: blast radius ممسوح، ومرور آلي يحوّل ما يستطيع ويؤشّر بأمانة على ما لا يستطيع، وقرارات يستطيع مهندسو المستقبل تتبعها.
المشروع الختامي هو التالي: قوس الوحدات الخمس كاملًا مدموجًا في مشروع هندسي واحد على codebase واحد، يُقيَّم أمام معايير رئيسية، ويقود إلى اعتماد «الهندسة مع Claude».