> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# Mars Climate Orbiter: مسبار ضاع بسبب وحدة قياس
- URL: https://arabroot.io/articles/مسبار-mars-climate-orbiter-ووحدات-القياس/
- Published: 2026-10-09T21:30:00.000Z
- Updated: 2026-10-09T09:47:07.000Z
- Description: كيف أضاع رقم بالرطل قرأه برنامج آخر بالنيوتن مسباراً كاملاً على المريخ، وكيف تمنع خطأ الوحدات في واجهات أنظمتك.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, قصص وتحقيقات, DevOps وCI/CD, أساسيات, الاستضافة الذاتية

في 23 سبتمبر 1999 وصل المسبار Mars Climate Orbiter إلى المريخ بعد تسعة أشهر في الفضاء، وبدأ محركه الرئيسي يعمل ليبطئ المسبار حتى تلتقطه جاذبية الكوكب، ثم اختفى خلف المريخ كما كان متوقعاً، ولكنه لم يظهر من الجهة الأخرى أبداً. وبعد ستة أيام اكتشف الفريق أن برنامجاً أرضياً كان يكتب طوال الطريق رقماً بوحدة الرطل قوة في الثانية pound-force seconds، بينما كان برنامج الملاحة يقرأ نفس الرقم على أنه بوحدة النيوتن في الثانية newton-seconds كما تنص مواصفة الواجهة بين الفريقين، فكان كل تصحيح صغير في المسار يحسب أصغر من حقيقته 4.45 مرة، وتراكم هذا الفرق حتى دخل المسبار الغلاف الجوي للمريخ على ارتفاع لا يتحمله.

وقد يبدو هذا خطأ لا يقع فيه إلا الغافل، ولكن لاحظ أن الرقم نفسه لم يكن خاطئاً، والكود الذي كتبه لم يكن فيه Bug بالمعنى المعتاد، وإنما كانت المشكلة في الواجهة Interface بين برنامجين كتبهما فريقان في شركتين مختلفتين، وهذا بالضبط ما يحدث كل يوم بين خدماتنا: حقل `timeout` بالثواني في خدمة وبالميلي ثانية في أخرى، أو مبلغ بالريال في نظام وبالهللة في نظام آخر، أو تاريخ بتوقيت محلي يقرؤه نظام يفترض UTC.

![ثلاثة مهندسين بملابس الغرفة النظيفة حول المسبار Mars Climate Orbiter على منصة اختبار، وفي وسطه طبق هوائي كبير مغلف بغشاء فضي وتحته خزانات مغطاة بغشاء ذهبي](https://arabroot.io/content/images/2026/10/mars-climate-orbiter-01-mco-tests.webp)

المسبار Mars Climate Orbiter أثناء اختبارات صوتية تحاكي ظروف الإطلاق في مايو 1998\. الصورة: NASA، ملكية عامة Public Domain، المصدر: [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Mars%5FClimate%5FOrbiter%5Fduring%5Ftests.jpg?ref=arabroot.io) (تم تصغيرها وتحويلها إلى WebP).

وسوف نناقش في هذا المقال ما يلي:

- ما هو المسبار، ومن كان يشغله، وما الذي حدث بالدقيقة يوم الوصول.
- السبب الأساسي Root Cause كما حدده تقرير لجنة التحقيق: البرنامج `SM_FORCES` وملف `AMD` ومواصفة الواجهة، ولماذا كان المعامل 4.45 بالذات.
- لماذا بقي الخطأ مخفياً تسعة أشهر رغم أن فريق الملاحة لاحظ شيئاً غريباً.
- الأسباب المساعدة Contributing Causes الثمانية في التقرير، ومنها مناورة تصحيح لم تنفذ.
- كيف تحمي الواجهات بين أنظمتك من خطأ الوحدات: من التعليق في الكود إلى الأنواع التي تحمل الوحدة.
- تجربة عملية Lab نكرر فيها الخطأ بأرقام عادية في Python، ثم نلتقطه بمكتبة pint وقت التشغيل، وبخاصية Units of Measure في F# وقت الترجمة.

الصورة التالية تبين الطريق الذي سلكه الرقم من محركات المسبار إلى حساب المسار، وأين دخلت الوحدة الخاطئة:

![خمس بطاقات مرقمة من اليمين إلى اليسار: 1 المسبار يشغل محركاته الصغيرة في عملية AMD وبرنامجه يحسب بالمتري، 2 البرنامج الأرضي SM_FORCES من Lockheed Martin يحسب الدفعة بوحدة lbf·s وهنا الخطأ، 3 ملف AMD فيه impulse_bit بقيمة 2.0 بدون وحدة ومواصفة الواجهة SIS تقول N·s، 4 فريق الملاحة في JPL يقرأ الرقم كأنه N·s فيحسب السرعة أصغر 4.45 مرة، 5 مسار محسوب خاطئ يصل إلى 57 كم بدل 226 كم، وتحتها بطاقتان: لماذا الرقم 4.45، وفرصة ضاعت لأن المسبار كان يرسل حسابه المتري الصحيح](https://arabroot.io/content/images/2026/10/mars-climate-orbiter-02-dataflow.webp)

الرقم انتقل بين برنامجين بدون وحدته، والمواصفة المكتوبة لم تكن كافية لتمنع ذلك

## ما هو Mars Climate Orbiter، ومن كان يشغله؟

كان Mars Climate Orbiter جزءاً من برنامج Mars Surveyor '98 في وكالة الفضاء الأمريكية NASA، ومعه مسبار آخر هو Mars Polar Lander الذي كان سيهبط على قطب المريخ الجنوبي، وبحسب [تقرير لجنة التحقيق في المرحلة الأولى Phase I Report](https://llis.nasa.gov/llis%5Flib/pdf/1009464main1%5F0641-mr.pdf?ref=arabroot.io) الصادر في 10 نوفمبر 1999، كان هدف المسبار أن يدور حول المريخ كأول قمر صناعي للطقس على كوكب آخر، وأن يكون محطة اتصال تنقل بيانات المسبار الهابط إلى الأرض. وقد أطلق في 11 ديسمبر 1998 من قاعدة كيب كانافيرال على صاروخ Delta II كما في [صفحة المهمة في موقع NASA](https://science.nasa.gov/mission/mars-climate-orbiter/?ref=arabroot.io)، وكانت كتلته عند الإطلاق 629 كيلوغراماً.

![صاروخ Delta II يرتفع عن منصة الإطلاق وسط سحابة كبيرة من الدخان واللهب، وعلى برج الإطلاق لوحة مكتوب عليها Mars Surveyor 98 Climate Orbiter](https://arabroot.io/content/images/2026/10/mars-climate-orbiter-03-launch.webp)

إطلاق Mars Climate Orbiter على صاروخ Delta II 7425 في 11 ديسمبر 1998\. الصورة: NASA/KSC، ملكية عامة Public Domain، المصدر: [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Mars%5FClimate%5FOrbiter%5F-%5Flaunch.jpg?ref=arabroot.io) (تم تصغيرها وتحويلها إلى WebP).

والنقطة المهمة لفهم الحادثة هي توزيع العمل، فالتقرير يذكر أن شركة Lockheed Martin Astronautics في دنفر Denver بولاية كولورادو كانت المقاول الرئيسي، وهي التي بنت المسبار وتشغله يومياً وترسل له الأوامر وتراقبه، بينما مختبر الدفع النفاث JPL التابع لـ NASA مسؤول عن المهمة كلها وعن الملاحة Navigation، أي حساب مكان المسبار ومساره من بيانات التتبع. وبالتالي هناك فريقان في مدينتين، وبينهما ملفات تنتقل من برنامج إلى برنامج، وكل ملف له مواصفة مكتوبة تسمى مواصفة واجهة البرمجيات Software Interface Specification واختصاراً SIS، تحدد شكل الملف ومعنى كل حقل فيه ووحدته.

## ماذا حدث يوم 23 سبتمبر 1999؟

قبل الوصول بأسبوعين، في 8 سبتمبر 1999، حسب فريق الملاحة آخر مناورة لتصحيح المسار Trajectory Correction Maneuver قبل المريخ، وهي الرابعة واسمها TCM-4، وكان الهدف أن تكون أقرب نقطة يمر بها المسبار من الكوكب Periapsis على ارتفاع 226 كم، وهو ارتفاع مناسب للمرحلة التالية التي يستخدم فيها المسبار احتكاكه بالغلاف الجوي العلوي ليخفض مداره تدريجياً Aerobraking، ونفذت المناورة في 15 سبتمبر كما خطط لها.

ولكن التقرير يذكر أن تقديرات فريق الملاحة في الأسبوع بين TCM-4 والوصول بدأت تنزل، فصارت أقرب نقطة بين 150 و170 كم، ثم قبل مناورة الدخول إلى المدار Mars Orbit Insertion واختصاراً MOI بساعة تقريباً، وبعد معالجة بيانات تتبع أدق مع اقتراب المسبار من جاذبية المريخ، نزل التقدير إلى 110 كم. وأقل ارتفاع يتحمله المسبار بحسب التقرير هو 80 كم، أي أن الرقم كان ما زال فوق الحد، فبدأت المناورة في موعدها:

- في الساعة 09:00:46 بتوقيت UTC بدأ تشغيل المحرك الرئيسي لمناورة الدخول إلى المدار.
- في الساعة 09:04:52 انقطعت الإشارة لأن المسبار دخل خلف المريخ Occultation، وهذا كان متوقعاً، ولكنه حدث قبل الموعد المحسوب بـ 49 ثانية، وهذه أول علامة أن المسبار أقرب من المتوقع.
- كان المفروض أن تعود الإشارة بعد 21 دقيقة، ولم تعد، واستمرت محاولات الاتصال حتى 25 سبتمبر دون نتيجة.
- في 27 سبتمبر جلس فريق الملاحة مع مهندسي المسبار لمناقشة الفروقات في حساب تغيرات السرعة، وفي 29 سبتمبر اكتشفوا أن تغيرات السرعة الصغيرة التي يأخذونها من ملفات المقاول أصغر من الحقيقة بمعامل 4.45.
- بعد تصحيح هذه القيم وإعادة حساب المسار بكل البيانات حتى لحظة انقطاع الإشارة، كانت أقرب نقطة 57 كم فقط.

والصورة التالية تبين كيف نزلت التقديرات من 226 كم إلى 57 كم مقارنة بالحد الأدنى الذي يتحمله المسبار:

![رسم أعمدة لارتفاع أقرب نقطة من المريخ: 226 km بعد المناورة TCM-4، ثم 150 إلى 170 km قبل الوصول بأسبوع، ثم 110 km قبل الوصول بساعة، ثم 57 km في إعادة الحساب بعد الحادثة، مع خط أحمر متقطع عند 80 km وهو أقل ارتفاع يتحمله المسبار، وبجانب الرسم أربع بطاقات تشرح كل رقم](https://arabroot.io/content/images/2026/10/mars-climate-orbiter-04-altitude.webp)

أرقام ارتفاع أقرب نقطة كما وردت في القسم الثاني من تقرير لجنة التحقيق

والتقرير يقول إن المسبار بهذا الارتفاع إما احترق في الغلاف الجوي أو عبره وخرج إلى مدار حول الشمس، وفي الحالتين انتهت المهمة، وقد ذكرت [صفحة المهمة في NASA](https://science.nasa.gov/mission/mars-climate-orbiter/?ref=arabroot.io) أن التحقيق وجد أن المسبار احترق في الغلاف الجوي للمريخ. وقد يتساءل البعض: إذا كان التقدير قبل ساعة 110 كم، فلماذا لم يكن ذلك كافياً للقلق؟ والإجابة أن الرقم كان ما زال فوق الحد الأدنى، ولم يكن أحد يعرف أن نموذج القوى الصغيرة كله خاطئ، فالتقدير 110 كم نفسه كان مبنياً على نفس الأرقام الخاطئة، وهذا هو الخطر الحقيقي للخطأ المنهجي Systematic Error: أنه يحرك كل القياسات في اتجاه واحد، فتبدو متسقة مع بعضها.

## السبب الأساسي: رقم بدون وحدة بين برنامجين

### لماذا يشغل المسبار محركاته طوال الطريق؟

يحافظ المسبار على اتجاهه في الفضاء بعجلات دوارة داخله تسمى عجلات رد الفعل Reaction Wheels، فعندما يريد أن يدور في اتجاه يغير سرعة العجلة في الاتجاه المعاكس، ولكن القوى الخارجية الصغيرة، وأهمها ضغط ضوء الشمس على الألواح الشمسية Solar Pressure، تجعل العجلات تتسارع شيئاً فشيئاً حتى تقترب من أقصى سرعتها. ولكي تعود العجلات إلى مجالها يشغل المسبار محركات دفع صغيرة Thrusters لفترة قصيرة، وتسمى هذه العملية تفريغ الزخم الزاوي Angular Momentum Desaturation واختصاراً AMD. ولاحظ أن هذه المحركات لا تدور بالمسبار فقط، وإنما تعطيه دفعة صغيرة تغير سرعته Delta-V، وهذه الدفعات الصغيرة يجب أن تدخل في حساب المسار وإلا تراكم الخطأ.

وهنا جاء أول عامل ضاعف المشكلة، فالتقرير يذكر أن عمليات AMD في Mars Climate Orbiter حدثت أكثر من المتوقع بـ 10 إلى 14 مرة، والسبب أن الألواح الشمسية في المسبار كانت في جهة واحدة من جسمه، بعكس المسبار السابق Mars Global Surveyor الذي كانت ألواحه متماثلة على الجانبين، فكان ضغط الشمس يولد عزماً أكبر يحتاج تفريغاً أكثر. والأسوأ أن الخطة الأصلية كانت تقلب المسبار 180 درجة كل يوم ليلغي هذا العزم، وهو ما سماه الفريق Barbecue Mode، ثم قررت دراسات هندسة النظم لاحقاً أنه غير ضروري فحذف من خطة التشغيل، ولم يبلغ فريق الملاحة بهذا القرار ولا بأثره على المسار. أي أن خطأ الوحدات لو حدث مع عدد قليل من عمليات AMD لربما كان أثره محتملاً، ولكن مع هذا العدد صار كل خطأ صغير يتكرر مئات المرات.

### البرنامج SM\_FORCES وملف AMD

بعد كل عملية AMD يرسل المسبار بياناتها إلى الأرض، وهناك برنامج أرضي اسمه `SM_FORCES` (اختصار Small Forces) يعالجها ويكتب النتيجة في ملف اسمه ملف AMD، وفريق الملاحة في JPL يقرأ هذا الملف ليضيف أثر الدفعات الصغيرة إلى نموذج المسار. والقيمة الأهم في الملف هي دفعة المحرك Impulse Bit، وهي القوة مضروبة في الزمن، ويحسبها البرنامج من بيانات أداء المحركات التي يعطيها المصنع.

وبحسب القسم الرابع من [تقرير اللجنة](https://llis.nasa.gov/llis%5Flib/pdf/1009464main1%5F0641-mr.pdf?ref=arabroot.io) فإن مواصفة الواجهة SIS التي تحدد شكل ملف AMD تنص على أن وحدة الدفعة هي النيوتن في الثانية N·s، وهي الوحدة المترية الصحيحة للدفعة، ولكن البرنامج الأرضي كتب القيمة بالوحدات الإنجليزية، أي بالرطل قوة في الثانية lbf·s، ثم قرأ برنامج الملاحة القيمة كما هي على أنها N·s، فقلل أثر كل تشغيل للمحركات على المسار بمعامل 4.45\. والتقرير يحدد السبب الأساسي في جملة واحدة: الفشل في استخدام الوحدات المترية في كود ملف أرضي اسمه Small Forces يستخدم في نماذج المسار.

والمعامل 4.45 ليس رقماً عشوائياً، فالرطل قوة pound-force هو القوة التي تؤثر بها الجاذبية الأرضية القياسية على كتلة رطل واحد، ويساوي 4.4482216152605 نيوتن كما في [جداول التحويل في دليل NIST](https://www.nist.gov/pml/special-publication-811/nist-guide-si-appendix-b-conversion-factors/nist-guide-si-appendix-b9?ref=arabroot.io)، فإذا كتبت الدفعة بالرطل وقرأتها على أنها بالنيوتن فقد قسمتها على هذا الرقم دون أن تشعر.

ولاحظ نقطة مهمة جداً يذكرها التقرير: البرنامج على المسبار نفسه كان يحسب الدفعة بالوحدات المترية وكان صحيحاً، والخطأ كان في البرنامج الأرضي فقط، بل إن المسبار كان يحسب تغير السرعة لكل عملية AMD ويرسله إلى الأرض، ولكن فريق الملاحة كما يقول التقرير لم يعرف ذلك إلا بعد الإطلاق بوقت طويل، ولو قارن أحد بين حساب المسبار وحساب البرنامج الأرضي مبكراً لظهر الفرق 4.45 مباشرة.

### لماذا بقي الخطأ مخفياً تسعة أشهر؟

قد يتساءل البعض: خطأ بمعامل 4.45 ليس صغيراً، فكيف لم يظهر في تسعة أشهر من التتبع؟ والإجابة في التقرير تجمع عدة أسباب متتالية:

1. **الملفات لم تستخدم أصلاً في أول أربعة أشهر:** كانت ملفات AMD الأولى فيها أخطاء في الصيغة Format وفي بيانات اتجاه المسبار Quaternion، فلم يستخدمها فريق الملاحة، وكان المقاول يرسل له رسالة بريد إلكتروني عند كل عملية AMD، والفريق يقدر أثرها بنفسه من توقيتها. واستغرق إصلاح الملفات أربعة أشهر، ولم يبدأ استخدامها إلا في أبريل 1999.
2. **ظهرت علامة خلال أسبوع ولم تحل:** بعد أسبوع تقريباً من استخدام الملفات الصحيحة لاحظ الفريق أن البيانات تشير إلى أن أثر عمليات AMD أقل من الحقيقة، ولكن هذه الملاحظة لم تحل.
3. **اتجاه الدفعة كان صعب الرصد:** فريق الملاحة يقيس سرعة المسبار من تغير تردد إشارته بتأثير دوبلر Doppler Shift، وهذا القياس يرى الحركة في اتجاه خط النظر بين الأرض والمسبار فقط، بينما كان الجزء الأكبر من دفعة المحركات عمودياً على خط النظر، وعمودياً أيضاً على اتجاه حركة المسبار، فكان الخطأ يدفع المسبار نحو المريخ دون أن يظهر بوضوح في القياس.
4. **الحلول المختلفة لم تتفق ولم يتابع أحد ذلك:** مع الاقتراب من المريخ استخدم الفريق ثلاث طرق لتحديد المدار، والحلول التي تعتمد على Doppler وحده كانت تشير دائماً إلى مسار أقرب إلى الكوكب، والتقرير يقول بوضوح إن هذه الفروقات لم تحل.

والصورة التالية تبين الأحداث بالترتيب من الإطلاق إلى تقرير اللجنة:

![خط زمني من اليسار إلى اليمين: 11 ديسمبر 1998 الإطلاق على Delta II، أول أربعة أشهر ملفات AMD لا تستخدم، أبريل 1999 الملفات الصحيحة تدخل الحساب وتظهر بيانات غريبة، 8 سبتمبر حساب TCM-4 والهدف 226 كم، 15 سبتمبر تنفيذ TCM-4 ثم تنزل التقديرات، 23 سبتمبر 09:00:46 بدء مناورة MOI والتقدير قبلها 110 كم وTCM-5 لم تنفذ، 09:04:52 UTC انقطاع الإشارة قبل الموعد بـ 49 ثانية، 29 سبتمبر اكتشاف المعامل 4.45، 10 نوفمبر 1999 تقرير لجنة التحقيق، وتحته أربع مراحل: تسعة أشهر في الطريق، الاقتراب، الوصول، التحقيق](https://arabroot.io/content/images/2026/10/mars-climate-orbiter-05-timeline.webp)

الخطأ كان موجوداً من أول يوم، ولكنه لم يدخل الحساب إلا من أبريل 1999، وكان يكبر مع كل عملية AMD

## الأسباب المساعدة: لماذا لم تلتقطه العمليات؟

أجمل جملة في التقرير في رأينا هي أن اللجنة تعترف بأن الأخطاء تحدث في مشاريع المركبات الفضائية، ولكن المشاريع عادة فيها عمليات كافية لالتقاط هذه الأخطاء قبل أن تصبح خطيرة، والمشكلة في Mars Climate Orbiter أن هذه العمليات لم تلتقط السبب الأساسي. لذلك لم يكتف التقرير بالوحدة الخاطئة، وإنما ذكر ثمانية أسباب مساعدة Contributing Causes:

| السبب المساعد في التقرير                                 | ماذا يعني عملياً                                                                                                                                                                                  |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1\. خطأ غير مكتشف في نمذجة تغيرات السرعة                 | نموذج القوى الصغيرة كان خاطئاً ولم يتحقق منه أحد بطريقة مستقلة.                                                                                                                                   |
| 2\. فريق الملاحة لا يعرف المسبار جيداً                   | فريق الملاحة في مرحلة التشغيل انضم قبل الإطلاق بقليل، ولم يشارك في اختبار البرامج الأرضية ولا في مراجعات التصميم الأولية والنهائية.                                                               |
| 3\. مناورة التصحيح الخامسة TCM-5 لم تنفذ                 | نوقشت شفهياً قبل مناورة الدخول بقليل، ولكن لم يكن هناك تحليل ولا إجراءات جاهزة لها، والجدول على المسبار لم يترك وقتاً لها.                                                                        |
| 4\. هندسة النظم لم تعالج الانتقال من التطوير إلى التشغيل | المعرفة التي عند فريق التطوير لم تنتقل إلى فريق التشغيل.                                                                                                                                          |
| 5\. ضعف التواصل بين أجزاء المشروع                        | فريق الملاحة لم يوصل قلقه بشكل فعال، واستخدم البريد الإلكتروني بدل نظام البلاغات الرسمي للحوادث والمفاجآت والأخطاء Incident, Surprise, Anomaly واختصاراً ISA.                                     |
| 6\. نقص في عدد فريق الملاحة                              | مشروع العمليات كان يشغل ثلاث مهمات في نفس الوقت، هي Mars Global Surveyor وMars Climate Orbiter وMars Polar Lander، وملاحة Mars Climate Orbiter قبل الوصول كان يتولاها قائد الفريق وملاح واحد فقط. |
| 7\. نقص في التدريب                                       | فريق البرنامج الأرضي كان يحتاج تدريباً على أهمية الالتزام بمواصفة الواجهة SIS، وعلى اختبار البرنامج من البداية إلى النهاية End-to-End.                                                            |
| 8\. التحقق Verification لم يغط البرامج الأرضية بشكل كاف  | مواصفة الواجهة كتبت ولكنها لم تستخدم في تطوير البرنامج واختباره، ولم يتضح أن اختباراً من البداية إلى النهاية أجري للبرنامج SM\_FORCES.                                                            |

ولاحظ في السبب الخامس تفصيلة يذكرها التقرير: فريق الملاحة افترض أن المسبار يشبه المسبار السابق Mars Global Surveyor في معظم أجزائه، فلم يتعمق في فهم المسبار الجديد وطريقة تشغيله، مع أن الألواح الشمسية غير المتماثلة كانت بالضبط ما جعل عمليات AMD أكثر بكثير. وهذا النوع من الافتراض، أي أن النظام الجديد يرث سلوك القديم، هو نفسه ما أسقط صاروخ Ariane 5 عندما نقل برنامج من Ariane 4 بافتراضاته، كما شرحنا في [قسم الحوادث في مقال Floating Point](https://arabroot.io/articles/%D8%A7%D9%84%D8%A3%D8%B1%D9%82%D8%A7%D9%85-%D8%A7%D9%84%D8%B9%D8%B4%D8%B1%D9%8A%D8%A9-floating-point-%D9%85%D9%86-%D8%A7%D9%84%D8%AF%D8%A7%D8%AE%D9%84/#incidents).

⚠️

لاحظ أن الفريق رأى العلامات: فروقات بين طرق تحديد المدار، وبيانات AMD غريبة من أبريل، وتقديرات تنزل في الأسبوع الأخير. والمشكلة لم تكن في غياب المعلومة، وإنما أن كل ملاحظة بقيت في رسالة بريد أو حديث جانبي ولم تتحول إلى بلاغ له صاحب وموعد إغلاق. لذلك إذا لاحظت في عملك رقماً لا يتفق مع رقم آخر فافتح له تذكرة Ticket في نظام التتبع عندكم، ولا تكتف برسالة في المحادثة.

## كيف تحمي الواجهات بين أنظمتك من خطأ الوحدات؟

الخطأ في Mars Climate Orbiter لم يكن في معادلة، وإنما في عقد Contract بين منتج Producer للبيانات ومستهلك Consumer لها، وكل خدمة تكتبها اليوم ترسل أرقاماً إلى خدمة أخرى عبر API أو ملف أو رسالة في Message Queue، لذلك سوف نمر على الحلول من الأبسط إلى الأفضل، ونرى أين يفشل كل منها:

1. **الحل البسيط هو أن تكتب الوحدة في التوثيق:** وهذا بالضبط ما فعله مشروع Mars Climate Orbiter، فالمواصفة SIS كانت تقول N·s بوضوح، ولكن المبرمج الذي كتب `SM_FORCES` لم يلتزم بها، ولم يكن هناك شيء في الكود أو الملف يجبره على ذلك. والتوثيق ضروري، ولكنه لا يعمل وقت التشغيل.
2. **الوحدة في اسم الحقل:** أن تسمي الحقل `impulse_ns` أو `timeout_ms` بدل `impulse` و`timeout`، وهذا تحسين كبير ورخيص، لأن من يقرأ الكود أو الملف يرى الوحدة دائماً، ولكنه ما زال اتفاقاً بشرياً، فلا شيء يمنع أحداً من أن يضع رقماً بالرطل في حقل اسمه `impulse_ns`.
3. **الوحدة جزء من القيمة:** أن ترسل القيمة ومعها وحدتها، مثل `{"value": 2.0, "unit": "lbf*s"}`، ثم يقوم المستهلك بالتحويل إلى الوحدة التي يحتاجها أو يرفض القيمة إذا كانت الوحدة غير معروفة، وهذا ما سوف نفعله في التجربة بمكتبة pint.
4. **الوحدة جزء من النوع Type:** في بعض اللغات تستطيع أن تجعل المترجم Compiler يرفض الكود إذا مررت `lbf·s` حيث المتوقع `N·s`، مثل خاصية [Units of Measure في F#](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/units-of-measure?ref=arabroot.io)، فيظهر الخطأ قبل أن يعمل البرنامج أصلاً.
5. **اختبار العقد واختبار النهاية إلى النهاية:** حتى مع كل ما سبق، الطريقة الوحيدة لتعرف أن المنتج والمستهلك متفقان هي أن تشغلهما معاً على بيانات معروفة النتيجة، وقد شرحنا [اختبار العقد Contract Test](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1%D8%A7%D8%AA-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-unit-%D9%88-integration-%D9%88-e2e/#contract) و[اختبار النهاية إلى النهاية End-to-End](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1%D8%A7%D8%AA-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-unit-%D9%88-integration-%D9%88-e2e/#e2e) في مقال أنواع الاختبارات، وهذا بالضبط الاختبار الذي قال التقرير إنه لم يجر للبرنامج `SM_FORCES`.

والقاعدة العملية: لا ترسل رقماً له وحدة عبر أي واجهة بدون أن تكون وحدته في اسم الحقل على الأقل، وعند تصميم واجهة جديدة اجعل الوحدة إما ثابتة في المخطط Schema ومكتوبة في الاسم، أو حقلاً مستقلاً يتحقق منه المستهلك، وقد ناقشنا تصميم الواجهات وتوثيقها في [مقال تصميم واجهات API](https://arabroot.io/articles/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%88%D8%A7%D8%AC%D9%87%D8%A7%D8%AA-api-rest-%D9%88-grpc-%D9%88-graphql/).

## التجربة العملية: نكرر الخطأ ثم نلتقطه

في هذه التجربة سوف نكتب نسخة صغيرة جداً من القصة: منتج يلعب دور `SM_FORCES` ويكتب ملفاً بصيغة JSON، ومستهلك يلعب دور برنامج الملاحة ويحسب تغير السرعة. والأرقام هنا للتوضيح فقط: قوة 0.2 رطل قوة لمدة 10 ثوان، والكتلة 629 كيلوغراماً وهي كتلة الإطلاق من التقرير، ولا علاقة لها بالأرقام الحقيقية لمحركات المسبار. نشغل كل شيء داخل Containers بإصدارات محددة: `python:3.13.16-slim` مع pint 0.26.1، و`mcr.microsoft.com/dotnet/sdk:10.0.100` لـ F#.

### الخطأ بأرقام عادية

الملف التالي `plain_floats.py` يكرر ما حدث: المنتج يحسب بالرطل، والمستهلك يفترض النيوتن، ولا يوجد شيء في الملف يقول غير ذلك:

```python
# The bug, with plain numbers: the producer writes lbf*s, the consumer assumes N*s.
import json

MASS_KG = 629.0            # MCO launch mass from the board's report (figure 1)

# Producer (plays the role of SM_FORCES): thruster data in English units
def sm_forces(firing_seconds):
    thrust_lbf = 0.2       # illustrative thrust per thruster, pound-force
    impulse = thrust_lbf * firing_seconds          # lbf*s, but nothing says so
    return {"event": "AMD-001", "impulse_bit": impulse}

# Consumer (plays the role of navigation): the interface spec says N*s
def navigation(amd):
    impulse_ns = amd["impulse_bit"]                # assumed N*s
    return impulse_ns / MASS_KG                    # delta-v in m/s

amd_file = json.dumps(sm_forces(firing_seconds=10))
print("AMD file      :", amd_file)
dv_model = navigation(json.loads(amd_file))
dv_real = 0.2 * 10 * 4.4482216152605 / MASS_KG
print(f"delta-v model : {dv_model*1000:.3f} mm/s")
print(f"delta-v real  : {dv_real*1000:.3f} mm/s")
print(f"ratio         : {dv_real/dv_model:.4f}")
```

نشغله في Container باسم `lab-disaster-mco-py` من مجلد التجربة:

```bash
docker run --rm --name lab-disaster-mco-py -v "$PWD:/lab" -w /lab python:3.13.16-slim python plain_floats.py
```

والمخرج سوف يكون كما يلي:

```text
AMD file      : {"event": "AMD-001", "impulse_bit": 2.0}
delta-v model : 3.180 mm/s
delta-v real  : 14.144 mm/s
ratio         : 4.4482
```

في المخرج أعلاه لاحظ التالي:

- الملف فيه `"impulse_bit": 2.0` فقط، ولا أحد يستطيع أن يعرف من الملف نفسه هل هي 2 نيوتن ثانية أم 2 رطل ثانية.
- البرنامج لم يرم أي خطأ ولم يطبع أي تحذير، وحسب 3.180 مم/ث بدل 14.144 مم/ث، والنسبة بينهما 4.4482، وهو نفس المعامل الذي وجدته لجنة التحقيق.
- تغير السرعة هنا ميليمترات في الثانية، أي رقم صغير جداً لا يلفت الانتباه، ولكن تذكر أنه تكرر مع كل عملية AMD طوال الطريق.

### نلتقط الخطأ وقت التشغيل بمكتبة pint

مكتبة [pint](https://pint.readthedocs.io/en/stable/?ref=arabroot.io) في Python تجعل كل رقم كمية Quantity لها قيمة ووحدة، فتحول بين الوحدات تلقائياً وترمي خطأ إذا جمعت أو مررت وحدتين لا تتوافقان. وفي الملف التالي `with_pint.py` يكتب المنتج الوحدة بجانب الرقم في الملف، ويستخدم المستهلك الدالة `ureg.check` الموثقة في [صفحة Wrapping and checking functions](https://pint.readthedocs.io/en/stable/advanced/wrapping.html?ref=arabroot.io) ليرفض أي قيمة ليست دفعة، أي قوة مضروبة في زمن:

```python
# The same interface with units carried by the value (pint).
import json
import pint

ureg = pint.UnitRegistry()
MASS = 629.0 * ureg.kilogram

def sm_forces(firing_seconds):
    thrust = 0.2 * ureg.pound_force
    impulse = thrust * firing_seconds * ureg.second
    # the file carries the unit next to the number
    return {"event": "AMD-001", "impulse_bit": {"value": impulse.magnitude, "unit": str(impulse.units)}}

@ureg.check("[force] * [time]")               # rejects anything that is not an impulse
def navigation(impulse):
    dv = (impulse / MASS).to("mm/s")          # converts whatever unit came in
    return dv

amd_file = json.dumps(sm_forces(10))
print("AMD file :", amd_file)
rec = json.loads(amd_file)["impulse_bit"]
impulse = ureg.Quantity(rec["value"], rec["unit"])
print("impulse  :", impulse, "=", impulse.to("N*s"))
print("delta-v  :", navigation(impulse))

print("1 lbf    =", (1 * ureg.lbf).to("N"))

# Mistake 1: a bare number where an impulse is expected
try:
    navigation(2.0)
except Exception as e:
    print("bare number ->", type(e).__name__ + ":", e)

# Mistake 2: force instead of impulse (forgot the seconds)
try:
    navigation(ureg.Quantity(0.2, "lbf"))
except Exception as e:
    print("force only  ->", type(e).__name__ + ":", e)

# Mistake 3: adding newton-seconds and plain numbers
try:
    total = impulse + 2.0
except Exception as e:
    print("add a float ->", type(e).__name__ + ":", e)
```

نثبت pint بإصدار محدد ثم نشغل الملف:

```bash
docker run --rm --name lab-disaster-mco-py -v "$PWD:/lab" -w /lab python:3.13.16-slim sh -c "pip install -q --root-user-action=ignore pint==0.26.1 && python with_pint.py"
```

والمخرج سوف يكون كما يلي:

```text
AMD file : {"event": "AMD-001", "impulse_bit": {"value": 2.0, "unit": "force_pound * second"}}
impulse  : 2.0 force_pound * second = 8.896443230521001 newton * second
delta-v  : 14.143788919747221 millimeter / second
1 lbf    = 4.4482216152605005 newton
bare number -> DimensionalityError: Cannot convert from '2.0' (dimensionless) to 'a quantity of' ([mass] * [length] / [time])
force only  -> DimensionalityError: Cannot convert from '0.2 force_pound' ([length] * [mass] / [time] ** 2) to 'a quantity of' ([mass] * [length] / [time])
add a float -> DimensionalityError: Cannot convert from 'force_pound * second' to 'dimensionless'
```

في المخرج أعلاه لاحظ التالي:

- الملف صار يحمل `"unit": "force_pound * second"`، والمستهلك لم يفترض شيئاً، وإنما حول القيمة إلى `8.896 newton * second` ثم حسب تغير السرعة الصحيح 14.144 مم/ث، أي أن المنتج بقي يحسب بالرطل كما يريد، والنتيجة صحيحة.
- تمرير رقم عادي `2.0` بدون وحدة رمى `DimensionalityError`، وهذا هو الخطأ الذي كان يجب أن يحدث في 1999 بدل أن يمر الرقم بصمت.
- تمرير قوة بدون زمن، أي نسيان ضرب الثواني، رمى أيضاً `DimensionalityError`، لأن الدالة تعرف أبعاد Dimensions الكمية المطلوبة وليس وحدتها فقط.
- جمع دفعة مع رقم عادي رمى خطأ أيضاً، فالمكتبة لا تسمح لك أن تخلط الأرقام التي لها وحدة بالأرقام التي ليس لها وحدة.

ولكن لاحظ أن pint تكشف الخطأ وقت التشغيل فقط، أي عندما يمر الكود بهذا السطر، فإذا كان المسار نادر الحدوث فقد لا تراه إلا في الإنتاج، وهنا يأتي دور المترجم.

### نلتقط الخطأ قبل التشغيل بخاصية Units of Measure في F#

في F# تستطيع أن تعرف وحدات القياس كجزء من النوع، فيصبح `float<N s>` نوعاً مختلفاً عن `float<lbf s>`، ولا يقبل المترجم أن تمرر أحدهما مكان الآخر. والملف التالي `bad.fsx` يكرر الخطأ نفسه: دالة الملاحة تطلب `N s`، والمنتج يعطيها `lbf s`:

```fsharp
// Units of measure: the compiler refuses lbf*s where N*s is expected.
[<Measure>] type kg
[<Measure>] type s
[<Measure>] type m
[<Measure>] type N = kg m / s^2
[<Measure>] type lbf

let mass = 629.0<kg>

// navigation: the interface says the impulse is in N*s
let deltaV (impulse: float<N s>) : float<m/s> = impulse / mass

// SM_FORCES: thruster data in pound-force
let impulseFromThrusters : float<lbf s> = 0.2<lbf> * 10.0<s>

printfn "delta-v = %f m/s" (deltaV impulseFromThrusters)
```

نشغله بالأمر [dotnet fsi](https://learn.microsoft.com/en-us/dotnet/fsharp/tools/fsharp-interactive/?ref=arabroot.io) الذي يترجم ملف السكربت ويشغله:

```bash
docker run --rm --name lab-disaster-mco-fs -e DOTNET_CLI_TELEMETRY_OPTOUT=1 -e DOTNET_NOLOGO=1 -v "$PWD:/lab" -w /lab mcr.microsoft.com/dotnet/sdk:10.0.100 dotnet fsi bad.fsx
```

والمخرج سوف يكون كما يلي:

```text
/lab/bad.fsx(16,36): error FS0001: Type mismatch. Expecting a
    'float<N s>'
but given a
    'float<lbf s>'
The unit of measure 'N s' does not match the unit of measure 'lbf s'
```

في المخرج أعلاه لاحظ التالي:

- البرنامج لم يعمل أصلاً، والخطأ `FS0001` في السطر 16 العمود 36، وهو مكان استدعاء `deltaV impulseFromThrusters`، والأمر انتهى برمز خروج 1.
- رسالة الخطأ تقول بوضوح إن المتوقع `float<N s>` والموجود `float<lbf s>`، أي أن الخطأ الذي كلف مسباراً كاملاً صار خطأ ترجمة يراه المبرمج في محرره قبل أن يحفظ الملف.
- الوحدات في F# موجودة وقت الترجمة فقط، فلا تكلف شيئاً وقت التشغيل، والرقم في الذاكرة `float` عادي.

والإصلاح هو أن نكتب معامل التحويل نفسه بوحدته `N/lbf`، فيتحقق المترجم من أن الضرب يعطي `N s` فعلاً، وهذا الملف `good.fsx`:

```fsharp
// The same code with an explicit, typed conversion factor.
[<Measure>] type kg
[<Measure>] type s
[<Measure>] type m
[<Measure>] type N = kg m / s^2
[<Measure>] type lbf

let mass = 629.0<kg>
let newtonsPerPound = 4.4482216152605<N/lbf>

let deltaV (impulse: float<N s>) : float<m/s> = impulse / mass

let impulseFromThrusters : float<lbf s> = 0.2<lbf> * 10.0<s>
let impulseSI : float<N s> = impulseFromThrusters * newtonsPerPound

printfn "impulse = %.4f lbf*s = %.4f N*s" (float impulseFromThrusters) (float impulseSI)
printfn "delta-v = %.3f mm/s" (float (deltaV impulseSI) * 1000.0)
```

```bash
docker run --rm --name lab-disaster-mco-fs -e DOTNET_CLI_TELEMETRY_OPTOUT=1 -e DOTNET_NOLOGO=1 -v "$PWD:/lab" -w /lab mcr.microsoft.com/dotnet/sdk:10.0.100 dotnet fsi good.fsx
```

والمخرج سوف يكون كما يلي:

```text
impulse = 2.0000 lbf*s = 8.8964 N*s
delta-v = 14.144 mm/s
```

ولاحظ أن النتيجة 14.144 مم/ث هي نفس نتيجة pint ونفس القيمة الحقيقية في التجربة الأولى، وأن المترجم لن يقبل أن تنسى معامل التحويل، لأن `lbf s` مضروبة في `N/lbf` هي وحدها التي تعطي `N s`.

وفي النهاية نحذف ال Image التي نزلناها للتجربة إذا لم تكن تحتاجها، علماً أن ال Containers حذفت تلقائياً بسبب الخيار `--rm`:

```bash
docker image rm mcr.microsoft.com/dotnet/sdk:10.0.100 python:3.13.16-slim
```

💡

إذا كنت تعمل بلغة أخرى فالفكرة نفسها موجودة بأشكال مختلفة: مكتبات للكميات والوحدات، أو أنواع صغيرة خاصة بك مثل `Milliseconds` و`Seconds` بدل `int` عادي، وحتى في لغة بدون أي من ذلك تستطيع أن تجعل الوحدة في اسم المتغير والحقل، وأن تحول كل القيم إلى وحدة واحدة عند حدود النظام ثم تعمل بها داخله.

## ماذا نتعلم كمهندسين؟

- **الواجهة أخطر مكان في النظام:** كل جزء من Mars Climate Orbiter كان صحيحاً وحده: المسبار يحسب بالمتري، وبرنامج الملاحة يقرأ المتري، والبرنامج الأرضي يحسب أرقاماً صحيحة بوحدته، والخطأ كان في الخط الذي يصل بينها، لذلك اختبر الأنظمة معاً وليس كل نظام وحده.
- **الرقم بدون وحدة نصف معلومة:** اجعل الوحدة في اسم الحقل أو بجانب القيمة أو في النوع، ولا تتركها في وثيقة منفصلة لا يقرؤها الكود.
- **البيانات المكررة فرصة للتحقق:** المسبار كان يرسل حسابه الخاص لتغير السرعة، ومقارنته بالحساب الأرضي كانت ستكشف الخطأ، فإذا كان عندك مصدران لنفس الرقم فقارن بينهما آلياً وأطلق تنبيهاً Alert عند الاختلاف.
- **الملاحظة الغريبة تحتاج بلاغاً رسمياً:** التقرير يذكر أن الفريق اعتمد على البريد الإلكتروني بدل نظام البلاغات، فبقيت المشكلة بين الشقوق كما وصفها، لذلك كل ملاحظة تقنية غريبة تحتاج تذكرة لها صاحب، وهي نفس الفكرة التي شرحناها في [دليل استكشاف الأخطاء](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/).
- **لا تفترض أن النظام الجديد يشبه القديم:** افتراض أن المسبار يشبه Mars Global Surveyor أخفى أن عمليات AMD ستكون أكثر بعشر مرات، فاكتب افتراضاتك صراحة وراجعها مع من بنى النظام الجديد.
- **جهز خطة الطوارئ قبل أن تحتاجها:** مناورة TCM-5 كانت ممكنة، ولكن لم يكن لها تحليل ولا إجراءات ولا معايير قرار، فعندما جاء وقتها لم يكن أحد مستعداً لتنفيذها، وكذلك خطة التراجع Rollback في نظامك يجب أن تكون مكتوبة ومجربة قبل يوم النشر.

## الخلاصة

- فقدت NASA المسبار Mars Climate Orbiter في 23 سبتمبر 1999 أثناء مناورة الدخول إلى مدار المريخ، بعد أن مر على ارتفاع قدر لاحقاً بـ 57 كم، بينما أقل ارتفاع يتحمله 80 كم والمخطط 226 كم.
- السبب الأساسي بحسب تقرير لجنة التحقيق في 10 نوفمبر 1999: البرنامج الأرضي `SM_FORCES` كتب دفعة المحركات في ملف AMD بوحدة lbf·s، بينما مواصفة الواجهة SIS وبرنامج الملاحة يتوقعان N·s، فقل أثر كل دفعة 4.45 مرة.
- تضاعف الأثر لأن عمليات AMD كانت أكثر من المتوقع بـ 10 إلى 14 مرة بسبب الألواح الشمسية غير المتماثلة، وبقي الخطأ مخفياً لأن الملفات لم تستخدم في أول أربعة أشهر، ولأن الدفعة كانت عمودية على خط النظر من الأرض.
- التقرير ذكر ثمانية أسباب مساعدة، منها غياب اختبار النهاية إلى النهاية للبرنامج الأرضي، وضعف التواصل والاعتماد على البريد الإلكتروني، ومناورة TCM-5 التي لم تنفذ.
- في التجربة رأينا الخطأ يمر بصمت مع الأرقام العادية، ثم التقطته pint وقت التشغيل بخطأ `DimensionalityError`، والتقطته F# وقت الترجمة بخطأ `FS0001`.
- الدرس لكل واجهة تكتبها: الوحدة في الاسم أو القيمة أو النوع، واختبار العقد بين المنتج والمستهلك، وبلاغ رسمي لكل رقم لا يتفق مع رقم آخر.

وإذا أردت أن تكمل من هنا، فهذه المقالات تشرح الاختبارات وتمثيل الأرقام والواجهات، وبقية كوارث هذه المرحلة:

[![](https://arabroot.io/content/images/2026/10/testing-types-cover-cover.webp)دليلأنواع الاختبارات للمبتدئين: من Unit إلى E2E مع Playwrightما الذي يكشفه كل نوع من الاختبارات وما الذي يفوته، وكم اختباراً تكتب من كل نوع، وأين يعمل كل منها في خط CI.](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1%D8%A7%D8%AA-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-unit-%D9%88-integration-%D9%88-e2e/)[![](https://arabroot.io/content/images/2026/10/floating-point-cover-cover.webp)دليلFloating Point من الداخل: كيف يخزن الحاسب الكسوركيف يحفظ معيار IEEE 754 الكسور في البتات، ولماذا تكبر الفجوة بين الأرقام وتضيع خانات المعرفات، ومتى تحتاج النوع العشري.](https://arabroot.io/articles/%D8%A7%D9%84%D8%A3%D8%B1%D9%82%D8%A7%D9%85-%D8%A7%D9%84%D8%B9%D8%B4%D8%B1%D9%8A%D8%A9-floating-point-%D9%85%D9%86-%D8%A7%D9%84%D8%AF%D8%A7%D8%AE%D9%84/)[![](https://arabroot.io/content/images/2026/10/number-systems-cover-cover.webp)دليلالأنظمة العددية للمبرمجين: من Binary إلى Floating Pointلماذا يظهر القرص 465 بدل 500، ويصبح العداد سالباً فجأة، ولا يساوي 0.1 + 0.2 القيمة 0.3؟ الإجابة في طريقة تخزين الأرقام.](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D8%B9%D8%AF%D8%AF%D9%8A%D8%A9-%D9%88%D8%A7%D9%84%D8%AB%D9%86%D8%A7%D8%A6%D9%8A-%D9%84%D9%84%D9%85%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/api-design-cover-cover.webp)دليلكيف تصمم API لا تضطر إلى كسره بعد سنةمبادئ تصميم API يتطور دون أن يكسر التطبيقات التي تعتمد عليه، وكيف تختار الشكل المناسب لنظامك.](https://arabroot.io/articles/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%88%D8%A7%D8%AC%D9%87%D8%A7%D8%AA-api-rest-%D9%88-grpc-%D9%88-graphql/)[![](https://arabroot.io/content/images/2026/10/att-1990-cover-cover.webp)دليلانهيار شبكة AT&T سنة 1990: تحديث برمجي أسقط 114 مقسماًكيف حول خلل في تحديث برمجي عطلاً عادياً في مقسم واحد إلى انهيار شبكة AT&T تسع ساعات، وماذا تتعلم عن اختبار مسار الاستعادة.](https://arabroot.io/articles/%D8%A7%D9%86%D9%87%D9%8A%D8%A7%D8%B1-%D8%B4%D8%A8%D9%83%D8%A9-att-%D8%B3%D9%86%D8%A9-1990/)[![](https://arabroot.io/content/images/2026/10/therac-25-cover-cover.webp)دليلTherac-25: حين أصبح خطأ برمجي خطراً على المرضىكيف تسبب سباق بين مهمتين وعداد من بايت واحد في جرعات إشعاع زائدة، وماذا تتعلم منها عن الأنظمة الحرجة وكودك اليومي.](https://arabroot.io/articles/%D8%AC%D9%87%D8%A7%D8%B2-therac-25-%D9%88%D8%A3%D8%AE%D8%B7%D8%A7%D8%A1-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D8%AD%D8%B1%D8%AC%D8%A9/)[![](https://arabroot.io/content/images/2026/10/ovh-fire-cover-cover.webp)دليلحريق OVHcloud في ستراسبورغ: نسخ احتياطية احترقت مع البياناتماذا حدث في حريق مركز بيانات OVHcloud سنة 2021، ولماذا فقد عملاء نسخهم الاحتياطية، وكيف تتأكد أن نسختك خارج الموقع فعلاً.](https://arabroot.io/articles/%D8%AD%D8%B1%D9%8A%D9%82-%D9%85%D8%B1%D9%83%D8%B2-%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-ovhcloud-%D9%81%D9%8A-%D8%B3%D8%AA%D8%B1%D8%A7%D8%B3%D8%A8%D9%88%D8%B1%D8%BA/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال اعتماداً على تقرير لجنة التحقيق في حادثة Mars Climate Orbiter، المرحلة الأولى (10 نوفمبر 1999)، وصفحة المهمة في NASA، مع تجربة عملية على Python 3.13.16 وpint 0.26.1 و.NET SDK 10.0.100 (F#)، ورسوم توضيحية من عملنا.