Skip to content

lessons learned

SalehGNUTUX edited this page Jul 22, 2026 · 2 revisions

دروسٌ مستخلَصة — خلاصةُ تجربةِ بناءِ تطبيقٍ تعليميٍّ محلّيٍّ للأطفال

ليست هذه قائمةَ مزالقَ تقنيّة (تلك في pitfalls-and-solutions)، بل دروسٌ في الحُكمِ والقرار — ما تعلّمناه عبرَ ستّةَ عشرَ إصدارًا عن بناءِ تطبيقٍ يعملُ دون إنترنتٍ ودونَ ذكاءٍ اصطناعيٍّ وقتَ التشغيل، لطفلٍ عربيٍّ بين الرابعةِ والعاشرة.

نُشارِكُها لأنّ أكثرَ ما كلّفَنا وقتًا لم يكنْ صعوبةً تقنيّةً، بل افتراضًا لم نتحقّقْ منه.


1) التكرارُ هو مصدرُ الأخطاءِ التي تُكلّفُ إصدارًا

ما حدث: أصلحْنا منطقَ اللاعبينَ في الوحدةِ المركزيّةِ versus.js. عملَ الإصلاحُ في صفحةِ اللغاتِ ولم يعملْ في صفحةِ الألعاب. السبب: صفحةُ الألعابِ كانت تحملُ نسخةً محلّيّةً من الدوالِّ نفسِها (ولعبةُ الذاكرةِ نسخةً ثالثة)، بينما صفحةُ اللغاتِ تستوردُ المركزيَّ فورِثتِ الإصلاحَ مجّانًا.

الدرس: قبلَ إعلانِ إصلاحٍ «شامل»، ابحثْ عن نسخٍ أخرى للدالّةِ نفسِها:

grep -rn "function <name>" --include=*.html --include=*.js .

وإن وجدتَ تكرارًا، فالإصلاحُ الحقيقيُّ هو حذفُ النسخةِ لا ترقيعُها.

وأثرٌ أخطر: كان منطقُ «مفتاحِ الطرفَين» في سجلِّ المبارياتِ مكرَّرًا في موضعَين. لو اختلفا قليلًا لَحذفَ زرُّ التصفيرِ سجلًّا غيرَ المقصود. صدّرْنا vsPairKey مصدرًا واحدًا يستعملُه التجميعُ والحذفُ معًا.


2) الاستثناءُ داخلَ حلقةِ إطارٍ يبتلعُ المنطقَ بصمت

ما حدث: «اللعبُ ضدّ الوقت» ينتهي وقتُه فلا يحدثُ شيء — لا حركةَ ولا انتقالَ دورٍ ولا ردَّ فعل. السبب: نداءُ sfx.wrong() ودالّةٌ بهذا الاسمِ غيرُ موجودةٍ أصلًا. الاستثناءُ وقعَ داخلَ requestAnimationFrame فأجهضَ بقيّةَ الشيفرةِ بهدوء.

الأخطرُ: بقيَ الخللُ في لعبتَينِ إصدارَينِ كاملَينِ دونَ أن يلاحظَه أحد، لأنّ «لا شيءَ يحدثُ» يبدو كمنطقٍ خاطئٍ لا كانهيار.

الدرس: حين لا يحدثُ شيءٌ فافحصِ الطرفيّةَ (console) أوّلًا لا الشيفرةَ. ثمّ اجعلْ ما يمكنُ فحصُه آليًّا مفحوصًا:

# قارِنْ كلَّ نداءاتِ sfx.X( بمُصدَّراتِ الوحدةِ فعلًا

3) الفحصُ الآليُّ أوفى من الوعدِ بالانتباه

ما حدث: طلبَ المستخدمُ مرارًا التزامَ الأيقوناتِ المعتمَدة، وكنتُ أُصلحُ ما يُشيرُ إليه ثمّ يتكرّرُ الأمر. حتى قال: «سئمتُ تذكيرَك».

ما فعلناه: بدلَ الاعتذارِ والوعد، بنينا أداةً (npm run check:icons) تفحصُ كلَّ الصفحاتِ وتفشلُ بخطأٍ إن وُجِدَ رمزٌ خامّ. كشفت فورًا ٢٤ رمزًا فاتَ التدقيقَ اليدويَّ مرّتين.

الدرس: أيُّ قاعدةٍ يُطلَبُ منك الالتزامُ بها مرّتين ⇒ حوّلْها إلى فحصٍ آليٍّ يفشل. الذاكرةُ البشريّةُ (والآليّة) ليست آليّةَ إنفاذ.

وحدُّ الأداة: check:icons تضمنُ أنّ الرمزَ معتمَد، لا أنّه مُنسَّقٌ صحيحًا — فقد ظهرت الأيقوناتُ ضخمةً تبتلعُ البطاقةَ رغمَ «نجاحِ» الفحص. التحقّقُ البصريُّ يبقى ضروريًّا.


4) وضعُ التطويرِ يقرأُ القرصَ، وgit يقرأُ الملتزَم

ما حدث: نشرْنا إصدارًا فاكتشفْنا أنّ الحزمَ تحوي مزايا لا يحويها الوسم. السبب: شجرةُ العملِ كانت فيها ملفّاتٌ معدَّلةٌ غيرُ مُلتزَمة. التحزيمُ يبني من القرص، بينما الوسمُ وموقعُ Pages يُبنيانِ من الملتزَم.

ولماذا لم يُكتشَفْ باكرًا؟ لأنّ npm run dev يقرأُ القرصَ — فكلُّ شيءٍ بدا سليمًا أثناءَ العمل.

الدرس: git status نظيفةً قبلَ التحزيم بندٌ في قائمةِ التحقّق. وما تراهُ في وضعِ التطويرِ ليس دليلًا على أنّه محفوظٌ في المستودع.


5) رقمُ الإصدارِ في ثلاثةِ مواضعَ لا موضعٍ واحد

package.json · package-lock.json · ونصُّ الواجهةِ المعروضُ للمستخدم. الثالثُ يُنسى لأنّه ليس بيانًا وصفيًّا بل نصٌّ في صفحة. نُشِرَ إصدارٌ كاملٌ يعرضُ رقمَ سابقِه.

الدرس: بندٌ صريحٌ في قائمةِ التحقّق + grep قبلَ الإصدار.


6) الأداءُ الصامتُ أسوأُ من العطلِ الصريح

جمعْنا أربعةَ نصوصٍ في سلسلةٍ واحدةٍ ونطقناها. كلُّ نصٍّ له مقطعُه الصوتيّ، لكنّ السلسلةَ المدموجةَ نصٌّ جديدٌ بلا مقطع — فارتدَّ التطبيقُ إلى نطقِ النظام. لم يُخطئْ شيءٌ ظاهريًّا: الصوتُ يعمل، لكنّه رديءٌ عربيًّا (وصامتٌ على بعضِ اللينكسات).

الدرس: حين يعتمدُ نظامُك على مطابقةٍ حرفيّة، فأيُّ تركيبٍ لحظيٍّ يكسرُها بصمت. اجعلِ الارتدادَ مرئيًّا أثناءَ التطوير، أو امنعِ التركيبَ أصلًا (roboSayChain).


7) في المحتوى العربيّ: التشكيلُ الجزئيُّ قد يكونُ أسوأَ من غيابِه

شكّلْنا النصوصَ جزئيًّا (٧٣٪) فتحسّنَ النطقُ في مواضعَ وساءَ في أخرى: التشكيلُ الناقصُ يُضلّلُ المُفنِّمَ فيُخطئُ حيثُ كان تخمينُه يُصيب.

ودرسٌ في القياس: فحصُنا الأوّلُ اعتبرَ كلمةً مثل معلّمي مشكولةً لأنّ فيها شدّة — وهي بلا حركاتٍ قصيرةٍ فتُنطَقُ خطأً. مقياسُك قد يكونُ هو الخطأ.

وقاعدةٌ لا تُخالَف: لفظُ الجلالةِ «الله» يُترَكُ مجرَّدًا؛ المشكَّلُ يُشوَّه.


8) صمّمْ للجهازِ الذي بيدِ الطفلِ لا لجهازِك

  • البوصلةُ لا تعملُ في متصفّحاتِ سطحِ المكتب (لا مستشعر) — فأخفينا الميزةَ هناك بدلَ عرضِ زرٍّ لا يعمل.
  • الاتجاهُ النسبيُّ (جيروسكوبيّ) يبدأُ صحيحًا ثمّ ينحرفُ تراكميًّا. رفضُ القراءةِ غيرِ المطلقةِ أصوبُ من بوصلةٍ كاذبة.
  • الطباعةُ على الهاتفِ لا تعملُ بـwindow.print() — لزِمَ توليدُ PDF ومشاركتُه.
  • زرُّ الرجوعِ في أندرويد: فتحُ رابطِ blob في نافذةٍ يُخرِجُ من التطبيقِ بلا عودة. العارضُ الداخليُّ أسلم.
  • الخطوطُ الحديثةُ للإيموجي قد تظهرُ «تُفو» على أجهزةٍ أقدم — فالأيقوناتُ SVG أضمن.

9) الاختبارُ على الجهازِ ليس اختياريًّا

قاعدةٌ ثابتةٌ عندنا: لا نشرَ حتى يختبرَ المستخدمُ على هاتفِه ويقول «ارفعْ وانشر». أكثرُ ما اكتُشِفَ من عللٍ حقيقيّةٍ جاء من لقطةِ شاشةٍ لا من قراءةِ شيفرة: أيقونةٌ ضخمة · «الوالدان» بدلَ «الأب» · نطقٌ خاطئٌ لكلمة · وميضٌ في الموضعِ الخطأ.

الدرس: لقطةُ شاشةٍ من المستخدمِ أثمنُ من ساعةِ مراجعةٍ ذاتيّة.


10) وثّقِ الجذرَ لا العَرَض

سجلُّ حالاتِنا مكتوبٌ بصيغةِ العَرَض ← الجذر ← الحلّ ← الموضع. وهذا ما جعلَه نافعًا فعلًا: بعدَ أشهرٍ لا تتذكّرُ ما فعلتَ، لكنّك تتعرّفُ على العَرَض حين يتكرّر.

والأهمّ: وثّقِ ما لم تفعلْه ولماذا (خيارٌ رُفِض، مسارٌ جُرِّبَ وفشل) — فذلك يمنعُ إعادةَ المحاولةِ عبثًا.


11) المبادئُ الصارمةُ تُبسّطُ القرارات

التزمْنا: محلّيٌّ ١٠٠٪ · بلا ذكاءٍ اصطناعيٍّ وقتَ التشغيل · بياناتُ الطفلِ لا تغادرُ الجهاز.

بدت قيودًا، فإذا هي مُيسِّرة: لا خوادمَ ولا مفاتيحَ ولا تكاليفَ تشغيلٍ ولا سياساتِ خصوصيّةٍ معقّدة، والتطبيقُ يعملُ في السيّارةِ وفي قريةٍ بلا شبكة. وحين يُقترَحُ حلٌّ يخالفُها يكونُ الجوابُ سريعًا وواضحًا.

الدرس: المبدأُ الواضحُ يُغني عن مئةِ نقاش — ويُوجِّهُ حتّى استثناءاتِه.

نظيرٌ في v1.7: أَذِنَ المستخدمُ لأوّلِ مرّة بميزةٍ عبرَ الإنترنت (القرآنُ الكامل)، فبقيَ المبدأُ حاكمًا في تفاصيلِها: مُطفأةٌ افتراضيًّا، خلفَ بوّابةِ الوالد، ولا تَمَسُّ الجزءَ المحلّيَّ القائم. الاستثناءُ المُتحكَّمُ فيه لا يَنقُضُ القاعدة؛ يُثبِّتُها. وكثيرٌ ممّا بدا يحتاجُ خدمةً (تقويمٌ هجريّ، سماتٌ، تتبّعُ عادات) تحقّقَ محلّيًّا: تحويلٌ هجريٌّ جدوليٌّ بمعادلة، لونٌ بمتغيّرِ CSS، سلسلةٌ محسوبةٌ من التقدّمِ القائم. حين يبدو المبدأُ عائقًا، الحلُّ غالبًا أبسطُ ممّا يُظَنّ.


12) أرقامُ التوثيقِ تَبلى بصمت

وجدْنا README يقولُ «٧١ ذكرية» والحقيقةُ ٨٥، و«٨ قصص» والحقيقةُ ٩. لا أحدَ يُخطئُ عمدًا — الأرقامُ تُكتَبُ مرّةً وينمو المحتوى.

الدرس: استخرِجِ الأرقامَ من الشيفرةِ عند التوثيقِ لا من الذاكرة:

node -e 'import("./content/library.js").then(m=>console.log(m.default.memories.length))'

خلاصةٌ في سطر

أكثرُ ما كلّفَنا لم يكنْ ما جهِلناه، بل ما ظنَنّا أنّنا نعرفُه ولم نتحقّقْ منه. فحوّلْ كلَّ افتراضٍ متكرّرٍ إلى فحصٍ آليّ، وكلَّ درسٍ إلى سطرٍ في سجلٍّ يقرؤُه من بعدَك.


رخصةُ هذه الوثيقة: GPL-3.0 — كجزءٍ من مِشكاةُ الطفلِ والآليّ. اقتبسْ منها بحرّيّة.

Clone this wiki locally