المصدر: docs-content/ar/05-decisions.mdتعديل هذه الصفحة

نُطقي — سجل القرارات (بأسلوب المحادثة)

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


D-01 — أين يعيش المشروع؟

س: مستودع جديد أم داخل مستودع قائم؟ وما الاسم؟ ج (المختار): مستودع أحادي جديد باسم nutqi، مُهيَّأ بـgit على الفرع main، مع خطّاف ما قبل الإيداع لفحص الأسرار مُتتبَّع في .githooks/. والاسم يعكس اسم المنتج، بصيغة kebab-case، بلا لواحق.

D-02 — أدوات المستودع الأحادي؟

س: Turborepo مع pnpm، أم Nx، أم مساحات عمل عادية؟ ج: مساحات عمل pnpm مع Turborepo. أخفّ إعداد يمنح تخزين نتائج المهام وعزل التطبيقات (apps/web، apps/api، apps/docs، packages/*). ورُفض Nx لأنه مبالغة لثلاثة تطبيقات.

D-03 — حزمة الواجهة الأمامية؟

س: إصدار Next.js وموجِّهه، والتنسيق، والمكوّنات؟ ج: Next.js 15 بموجِّه التطبيقات مع TypeScript وTailwind CSS الإصدار الرابع. الرموز مأخوذة من نظام تصميم Figma ومربوطة بسمة Tailwind (تدرّج Fayrouz لونًا أساسيًا). وطبقة المكوّنات مكتوبة يدويًا (packages/ui) اتّباعًا لصفحات Figma بدلًا من shadcn — فلغة التصميم (بطاقات بحدود فيروزية، وتبويبات على هيئة أقراص، ورقاقات من اليمين إلى اليسار) خاصة بما يكفي لأن يكلّف تطويع عناصر shadcn أكثر من كتابة مكوّنات رفيعة.

D-04 — حزمة الواجهة الخلفية؟

س: بنية NestJS، وأداة تعيين الكائنات، وقاعدة البيانات؟ ج: NestJS 11 (تطبيق أحادي مقسَّم إلى وحدات) مع Prisma و**PostgreSQL 16**. وحدة لكل مجال (auth، users، patients، specialists، centers، bookings، plans، payments، hr، jobs، reviews، notifications). وواجهة REST ببادئة عامة /api/v1، وSwagger على /api/docs، وكائنات نقل بيانات موثَّقة بـclass-validator.

D-05 — نموذج المصادقة؟

س: جلسات أم JWT؟ وهل يدخل Google OAuth في الإصدار الأول؟ وما مزوّد رمز التحقق؟ ج: رمز وصول JWT لمدة خمس عشرة دقيقة مع رمز تحديث دوّار لمدة ثلاثين يومًا داخل ملفات تعريف ارتباط httpOnly، على الأصل نفسه عبر إعادة كتابة Next (/api/* → واجهة البرمجة) وفق قواعد التطوير العامة للجهاز. أما Google OAuth فـصوري خلف راية ميزة (الزر يُعرض، والتلميح معطَّل ونصه قريباً) — إذ لا توجد مفاتيح بعد؛ وهذا موثَّق. ورمز التحقق: واجهة OtpChannel قابلة للتوصيل مع مزوّد تطويري يطبع الرمز في الطرفية — ومزوّدات SMS وWhatsApp إعدادات لا كود، في وقت لاحق.

D-06 — الأدوار؟

س: التصميم يعرض سبع تجارب — كيف نُنمذجها؟ ج: User.role ∈ {ADMIN, PERSONAL_PATIENT, GUARDIAN, SPECIALIST, CENTER_OWNER, CENTER_MANAGER, SECRETARY, CENTER_SPECIALIST}؛ ويُضاف لموظفي المركز صف في CenterStaff يربط المستخدم بالمركز وبالفرع. ويتشارك وليّ الأمر والمريض الشخصي بوابة المريض نفسها (المريض الذاتي = مريض قيمة guardianId == null لديه).

D-07 — الخطوط؟

س: Figma يستخدم خط عرض عربيًا مستديرًا؛ فما الذي يُشحن؟ ج: Baloo Bhaijaan 2 (من Google Fonts، يدعم العربية واللاتينية ويطابق العناوين المستديرة) للعرض؛ و**IBM Plex Sans Arabic** للمتون والجداول (أوضح في النصوص الطويلة). وكلاهما عبر next/font بمجموعتَي المحارف latin وarabic.

D-08 — التدويل؟

س: المكتبة، واللغة الافتراضية، واستراتيجية الروابط؟ ج: next-intl، باللغتين ar (الافتراضية، من اليمين إلى اليسار) وen (من اليسار إلى اليمين)، باستراتيجية بادئة /{locale}/… مع ar افتراضيةً. وسمة <html dir> تتبع اللغة؛ وكل التباعد بخصائص CSS المنطقية. وملفات الرسائل مقسَّمة بمساحات أسماء تحت apps/web/messages/{ar,en}/.

D-09 — مواعيد الحجز؟

س: شبكة المواعيد بمربعات الاختيار في التصميم — اختيار متعدد أم واحد؟ ج: موعد واحد لكل حجز (بسلوك زر الاختيار، وبواجهة رقاقات). فالقراءة متعددة المواعيد لتصميم مربعات الاختيار تخلق التباسًا في السعر والمدة. وتُولَّد المواعيد من ساعات عمل الأخصائي (شبكة من ثلاثين دقيقة) ناقص الحجوزات القائمة.

D-10 — نطاق المدفوعات؟

س: بوابة دفع حقيقية؟ وحسابات المحفظة؟ ج: لا بوابة دفع في الإصدار الأول. الحجوزات تحمل سعرًا؛ والمدفوعات تُسجَّل (نقدًا أو داخل المركز أو بتحويل يدوي) بواسطة السكرتير أو الأخصائي؛ ويرى وليّ الأمر دفتر أستاذ (الإصلاح G01)؛ ومحفظة الأخصائي تجمع الأرباح ناقص رسوم المنصة (إعداد، والافتراضي صفر) مع طلبات سحب يدوية (الإصلاح G19). والبوابة (Paymob/Stripe) واجهة صورية.

D-11 — تطبيق التوثيق؟

س: Nextra أم تطبيق مخصَّص؟ ج (مُعدَّل أثناء التنفيذ): كان Nextra 4 في الأصل — لكن ملفات التوثيق والمواصفات تحتوي على أقواس معقوفة خامًا لكائنات نقل البيانات مثل { patientId, ... } خارج كتل الكود، وMDX يحلّلها تعبيرات JSX → فيفشل بناء Nextra، وقد يكسره من جديد أي تعديل محتوى لاحق. وما شُحن بدلًا منه: Next.js 15 بتوليد صفحات ساكنة مع react-markdown/remark-gfm (لا يفشل أبدًا على محتوى عشوائي) وmermaid@11 على جانب العميل للمخططات، وشريط جانبي وهيكل نصي مكتوبان يدويًا، وdir="auto" لكل كتلة للمحتوى المختلط. ويبقى docs-content/ المصدر الوحيد للحقيقة، يُقرأ من جذر المستودع وقت البناء (بلا نسخ).

D-12 — الاختبارات وبوابات الجودة؟

س: ما الذي يُثبت صحة البناء؟ ج: واجهة البرمجة: Vitest لاختبارات الوحدة والاختبارات الشاملة (بـsupertest مقابل تطبيق Nest، مع حاوية اختبار PostgreSQL خالية من SQLite → والرجوع إلى PostgreSQL عادية في بيئتَي التكامل المستمر والتطوير). والويب: Vitest مع Testing Library للمكوّنات والأدوات، مع اختبار دخان بـPlaywright (مسار المصادقة والحجز السعيد) يُعدّ اختياريًا إن لم تتوفر المتصفحات. والبوابات: pnpm lint وpnpm typecheck وpnpm test وpnpm build كلها خضراء من الجذر عبر turbo.

D-13 — خوادم التطوير؟

ج: وفق قواعد الجهاز: portless بأسماء المضيفين nutqi-web وnutqi-api وnutqi-docs؛ بلا منافذ مثبَّتة في السكربتات؛ وAPI_URL موصولة عبر portless get nutqi-api في سكربت تطوير الويب؛ وCORS_ORIGIN تتضمن https://nutqi-web.localhost.

D-14 — تعدادات الحالة؟

ج: التعدادات المعتمدة (بتسميات عربية في الواجهة): الحجز PENDING(معلق) / WAITING(انتظار) / POSTPONED(تأجيل) / DONE(تم) / CANCELLED(ملغي)؛ وإسناد البرنامج NOT_ANSWERED(لم يتم) / ANSWERED(تم)؛ وطلب الوظيفة PENDING(مُعلق) / ACCEPTED(تم القبول) / REJECTED(تم الرفض)؛ والحساب PENDING_ACTIVATION / ACTIVE / SUSPENDED؛ وطلب الموارد البشرية PENDING / APPROVED / REJECTED.

D-15 — مجموعة عناصر الأسئلة؟

ج: SHORT_TEXT, LONG_TEXT, SINGLE_CHOICE, MULTI_CHOICE, YES_NO, STARS, LIKERT, MATRIX, PERCENT, DATE, FILE — وهي بالضبط ما يعرضه إطار البرنامج غير المُجاب عنه؛ ومخطط الكائن النصي للقالب مُرقَّم بإصدار (schemaVersion) للتوافق المستقبلي.

D-16 — أي المسارات نال تحسينًا في الواجهة الأمامية وحدها خارج التصميم؟

ج: قائمة E01E15 في 04-gaps-and-fixes.md؛ وأكثر ثلاثة تغيّر الإحساس: الحفظ التلقائي للاستبيان (E01)، ورقاقات الحالة المتفائلة (E04)، ولوحة الأوامر ⌘K (E06).

D-17 — تقليصات نطاق الإصدار الأول (صراحةً):

ج: لا مكالمات فيديو حقيقية (حقل رابط فقط)، ولا بوابة دفع، ولا إشعارات فورية (قناتا البريد والطرفية موصولتان؛ وWhatsApp وTelegram خلف واجهة)، ولا صفحات مراكز عامة (G16 → خارطة الطريق)، ولا واجهة ويب للمشرف (الإدارة عبر واجهة البرمجة والبذور؛ والشاشات في خارطة الطريق)، وصفحة الموقع في Figma متجاهَلة وفق التعليمات.

D-18 — بيانات البذور؟

ج: بذرة عرض ثنائية اللغة: مشرف واحد، ووليّا أمر (بثلاثة أطفال)، ومريض شخصي واحد، وأخصائيان مستقلان (واحد مُفعَّل وآخر بانتظار التفعيل)، ومركز واحد (مالك، ومدير، وسكرتير، وأخصائيا مركز، وفرعان)، وحجوزات بكل الحالات، وقالب برنامج واحد مع إسنادات مُجاب عنها وأخرى غير مُجاب عنها، وتقييمات، ومعاملات محفظة، وإعلان وظيفة مع طلبات، وسجلات موارد بشرية. وتتشارك كل حسابات العرض كلمة سر تطويرية فقط، موثَّقة في ملف README بالمستودع (ولا تُستخدم خارج التطوير المحلي).

D-19 — مصدر الحقيقة لرموز التصميم؟

ج: packages/ui/tokens.css (متغيرات CSS) مع ربطها بسمة Tailwind. وتدرّجا Fayrouz وblue من 50 إلى 950 مأخوذان من أطقم الظلال في نظام التصميم؛ والأسماء الدلالية (--color-primary، --color-danger، --color-dashboard-bg، --color-text، --color-border) تطابق أسماء الأنماط في Figma (Fayrouz، blue، wrong color، dashboard، text color، border color).

D-20 — تثبيت مدير الحزم؟

ج (مُعدَّل): pnpm 11 (الإصدار 11.10.0 مثبَّت على الجهاز — والتوصية الأصلية قالت 9؛ والأحدث مقبول ومثبَّت عبر packageManager). وNode بإصدار 20.19 أو أعلى (الجهاز يشغّل 24). وملف .npmrc مُتتبَّع بخيار save-exact.

D-24 — استضافة المستودع وصقل التوثيق (جولة موجَّهة للعميل)؟

س: مستودع GitHub عام أم خاص؟ وما الذي يتغيّر في موقع التوثيق الموجَّه للعميل؟ ج: مستودع خاص (nutqi على GitHub) — فهو مشروع عميل، والكود ليس للتصفّح العام؛ وقد جرى تجاوز افتراضي الجهاز العام عن قصد. وحصل موقع التوثيق على جولة صقل موجَّهة للعميل: تنظيف التفاصيل الداخلية للتطوير (المسارات المحلية وكلمات سر البذور) من المحتوى؛ وعزل ثنائي الاتجاه مع تباعد يُحسِّن قراءة الأسطر التي تخلط العربية داخل الإنجليزية؛ ومؤشّر تمرير للأقسام في كل صفحة (فهرس متتبِّع للتمرير)؛ وروابط «تحرير على GitHub» لكل صفحة؛ وبحث سريع عام (⌘K)؛ واستبدال علامة الشعار النائبة بعلامة اسمية محايدة ريثما يصل أصل العلامة الحقيقي.

D-25 — انحرافات بوابتَي الأخصائي والمركز؟

س: ما الذي تغيّر أثناء بناء بوابتَي الطاقم؟ ج: اكتسب الشريط الجانبي للأخصائي مدخل قائمة «المرضى» (فالملفات تحتاج فهرسًا؛ بالسابقة نفسها التي أعطت وليّ الأمر مدخل الدليل). وعناصر التنقل مُرشَّحة بالدور (الخدمة الذاتية للموارد البشرية → أخصائيو المركز؛ والطاقم والتوظيف → المالك والمدير؛ والحضور → السكرتير). وتبويبات الإعدادات الثلاثة (كلمة السر واللغات والإشعارات) مكوّنات مشتركة يُعاد استخدامها في كل البوابات. ووسائل السحب مُعدَّدة على جانب البوابة: تحويل بنكي / محفظة إلكترونية / InstaPay (تركتها المواصفة مفتوحة). وإحصاءات لوحة المركز تُحسب على جانب العميل من نقاط نهاية القوائم ريثما تتوفر نقطة نهاية مخصَّصة للإحصاءات. وطُلبت متابعتان في واجهة البرمجة: كشف نقاط قراءة لجدول الأخصائي وأيام إجازته وإعدادات حجزه وشهاداته وفيديوهاته، وتضمين templateId في حمولات قائمة الإسنادات.

D-22 — استراتيجية بناء واجهة البرمجة (انحراف تنفيذي)؟

س: حزمة @nutqi/shared تشحن TypeScript خامًا — والإخراج بـtsc لكل ملف سيترك require("@nutqi/shared") في وقت تشغيل واجهة البرمجة مشيرًا إلى ملفات .ts. ج: حزمة webpack من واجهة Nest السطرية (webpack-node-externals مع قائمة سماح لـ@nutqi/*) تجمع الكود المشترك داخل dist/main.js؛ وتُبقي مسارات paths في tsconfig فحص الأنواع أمينًا. ومتابعة في المرحلة الثانية: واجهة Nest السطرية 11 لم تعد تشحن ts-loader (وهو غير قابل للحل تحت عزل pnpm الصارم)، فصار البناء يستعمل مُحمِّل webpack محليًا من نحو ثلاثين سطرًا يعتمد على @swc/core الموجود أصلًا (عائلة التحويل نفسها المستعملة في خط الاختبارات) — بلا أي اعتماديات جديدة. وأيضًا: اكتسب WorkInfo حقل deletedAt (فالمواصفة 00 تُدرج بيانات العمل ضمن الحذف الناعم، واختصار المواصفة 01 أغفلها) مع نقاط استعادة وفق G18. ورموز التحديث أزواج مبهمة rowId.secret (مُجزَّأة بـsha256 عند التخزين، والتدوير يُبطلها)؛ وتفرُّد المواعيد مفروض داخل معاملة (فـPrisma تفتقر إلى الفهارس الفريدة الجزئية)؛ ومحفظة EARNING تُقيَّد صافيةً بعد رسوم المنصة؛ والسحب يُخصم فقط حين يضع المشرف علامة TRANSFERRED.

D-23 — انحرافات تنفيذ الواجهة الأمامية؟

س: ما الذي تغيّر بين المواصفة 03 والأساس المشحون؟ ج: (1) البوابات تعيش على بادئات روابط حقيقية /guardian و/specialist و/center — فمجموعات المسارات الصرفة (guardian)… كانت ستتصادم على مسارات متطابقة (/dashboard ثلاث مرات يكسر بناء Next). (2) الاستيفاءات العددية بصيغة ICU تُمرَّر قيمًا نصيةً مسبقًا كي تبقى الأرقام لاتينية في العربية وفق قرار الأرقام. (3) تسميات الحالة تأتي مباشرة من خرائط @nutqi/shared — ولا تُكرَّر أبدًا في ملفات الرسائل (G21). (4) تصدير Excel = ملف CSV بعلامة ترتيب البايت UTF-8، وPDF = نمط طباعة. (5) رفع مستند ترخيص المركز يحدث بعد تسجيل الدخول (فالتسجيل يجمع الرقم فقط). (6) الوسيط يتحقق من وجود ملف تعريف الارتباط؛ وتخطيطات البوابات تُجري فحص الدور الحقيقي — تفتح إن تعذّر الوصول لحظيًا إلى واجهة البرمجة، وتُغلق عند دور خاطئ. (7) اكتسب الشريط الجانبي لوليّ الأمر عنصرًا سابعًا هو «الأخصائيون» لاكتشاف الأخصائيين (G12).

D-21 — التكامل والتسليم المستمران (سأل مالك المنتج في منتصف البناء)؟

س: أي أداة تكامل مستمر، وأي أهداف تسليم؟ ج: GitHub Actions بمهمة verify واحدة: pnpm مع ذاكرة turbo المؤقتة، وخدمة PostgreSQL 16، وبوابات linttypechecktestbuild (.github/workflows/ci.yml)، مع إلغاء التزامن لكل مرجع. وTurborepo يحافظ على الحدود: كل تطبيق أو حزمة يعلن مهام lint/typecheck/test/build الخاصة به؛ والجذر يشغّلها عبر turbo run مع التخزين المؤقت. التسليم: الويب والتوثيق يُنشران على Vercel موصولةً بمستودع GitHub (معاينة تلقائية لكل طلب دمج، وإنتاج على main) — أُنشئت عند أول نشر وفق قواعد النشر على الجهاز؛ أما واجهة NestJS فلا مضيف مُجهَّز لها بعد، فتسليمها متابعة موثَّقة (الخيارات: Railway أو Fly أو خادم خاص افتراضي عبر Docker؛ وملف Dockerfile مشحون في apps/api).

D-26 — من يُسطِّح البيانات: واجهة البرمجة أم البوابات؟

س: تشغيل التطبيق من طرف إلى طرف كشف صنفًا كاملًا من الأعمدة الفارغة وصفحةً واحدة تنهار. كانت واجهة البرمجة تعيد صفوف قاعدة البيانات وعلاقاتها ما تزال متداخلة (booking.specialist.user.firstName)، بينما يتوقع كل جدول في البوابات صفًا مسطَّحًا (specialistName). فمن يتكيّف؟ ج: واجهة البرمجة هي التي تتكيّف — مُحوِّل واحد لكل وحدة، يُطبَّق على كل مسار قراءة. كل وحدة تحوّل الآن صفوفها إلى الشكل الذي تَعِد به المواصفة 02 قبل الرد: الأسماء تُحلّ إلى سلسلة واحدة، وتواريخ التقويم بصيغة YYYY-MM-DD (لا طوابع زمنية أبدًا)، والصور الرمزية روابط ملفات جاهزة للاستعمال، والعدّادات (applicationsCount، sessionsCount، bookingsCount) تُحسب على الخادم، والعلاقات تُسقَط كي لا يتسرّب تخطيط قاعدة البيانات إلى العميل. وطُبِّق ذلك على الحجوزات، والبرامج والتقييمات التشخيصية، والمدفوعات، ودليل الأخصائيين، والطاقم، والمرضى، والوظائف، وطلبات الوظائف، وجداول الموارد البشرية الخمسة. وفعلُه مرة واحدة في واجهة البرمجة يُبقي كل بوابة (وتطبيق الهاتف مستقبلًا) تقرأ الحمولة نفسها بدل أن تعيد كل شاشة اشتقاق الأسماء.

وسقطت من الجولة نفسها ثلاثة إصلاحات ذات صلة: (1) التخصص — يكتب الأخصائي تخصصه نصًا حرًا (تخاطب)، بينما يعرض مُرشِّح الدليل أنواع جلسات ثابتة، فلا يتطابق الطرفان أبدًا؛ وصار كلاهما يُحلّ عبر قائمة التسميات المشتركة، وتختفي الرقاقة ببساطة للنص الحر الذي ليس نوع جلسة معروفًا. (2) مسمّيات الوظائف (دكتور / أخصائي) صارت تحصل على تسميات عربية وإنجليزية سليمة من ملف التسميات المشترك بدل عرض الرمز الخام (G21 — فالتسميات لا تعيش أبدًا في ملفات الترجمة). (3) حلقة التفعيل في لوحة الأخصائي كانت تقرأ حقلًا باسم مختلف عمّا ترسله واجهة البرمجة، فكانت تعرض صفرًا بالمئة دائمًا وتُبقي الأخصائيين المُفعَّلين بالكامل خلف بوابة «أكمل ملفك الشخصي»؛ وتطابقت الأسماء الآن فانفتحت البوابة كما يجب.

وضمانات كي لا يعود ذلك بصمت: اختبارات العقد تتحقق من الشكل المسطَّح على نقاط الحجز ودفتر الأستاذ والإسناد والدليل والحضور — بما في ذلك غياب العلاقات — ومكوّن الصورة الرمزية المشترك صار يتراجع إلى الأحرف الأولى بدل أن يُسقط الصفحة حين يغيب الاسم.

D-27 — فاتح أم داكن افتراضيًا؟

س: تصميم Figma فاتح فقط، لكن كل منتج حديث يشحن مظهرًا داكنًا، والمتصفح يُعلن التفضيل أصلًا عبر prefers-color-scheme. فهل نتبع نظام التشغيل؟ وماذا يحدث للفيروزي في الظلام؟ ج: مظهران، light وdark، الافتراضي منهما light، وبلا تبنٍّ تلقائي لتفضيل نظام التشغيل. فالزائر لأول مرة يحطّ دائمًا على الفاتح ولو كان جهازه يفضّل الداكن؛ ولا يبدّل المظهر إلا اختيار صريح من المستخدم. والمظهر النشط سمة واحدة على العنصر الجذر — <html data-theme="light"> / <html data-theme="dark"> — ولا شيء غير ذلك (لا class="dark"، ولا next-themes). والخادم يعرض دائمًا data-theme="light"، وهو ما يُبقي كل مسار قابلًا للتوليد الساكن (بلا cookies()، وبلا اشتراك ديناميكي)، ثم يرفع سكربتٌ صغير حاجب بلا اعتماديات داخل <head> قيمةَ السمة قبل أول رسم متى احتفظ localStorage["nutqi-theme"] بالقيمة "dark" (وغياب المفتاح ⇒ فاتح). ولا ينقلب إلا الطابق الدلالي: تدرّجات العلامة (--fayrouz-*، --blue-*) ثابتة بين المظهرين، و:root يحمل قيم الفاتح، و[data-theme="dark"] يتجاوز الدلالات وحدها — --surface و--surface-2 و--overlay، وعائلة --primary-hover/-active/-fg/-soft/-muted/-border، وأزواج نغمات رقاقة الحالة الخاصة بـG21، وظلال الارتفاع؛ أما أنصاف الأقطار فلا تتغيّر أبدًا. ونتيجة ذلك حظرُ أدوات الألوان الحرفية في كود التطبيق: bg-whitebg-(--color-surface)، وخلفيات bg-black/40bg-(--color-overlay)، وtext-primary-600text-(--color-primary-fg)؛ تفرضه بوابة جديدة apps/web/scripts/check-literal-colors.mjs موصولة بمهمة lint في الويب، مع قائمة سماح للأسطح الثابتة عن قصد (فلوحة المصادقة المشطورة وواجهة الهبوط تقعان على تدرّج فيروزي ثابت في المظهرين، فتبقى text-white فيهما — ويحمل كل حرفيّ مُبقًى تعليقًا يوضّح ذلك). ويتشارك apps/web وapps/docs المفتاح والسمة والآلية نفسها، غير أن التوثيق يشحن مكوّن ThemeToggle محليًا خاصًا به بدل أن يكتسب اعتمادية على @nutqi/ui. المواصفة: specs/07-theming.md.

D-28 — موقع توثيق واحد مختلط، أم نسختان غير ممزوجتين؟

س: ملفات docs-content/*.md نثر إنجليزي مبثوثة فيه أسماء شاشات عربية — وعبارة مثل الإحصائيات (specialist home) تفرض إعادة تدفّق ثنائي الاتجاه في منتصف الجملة وتُقرأ بصعوبة لدى الجمهورين. فهل نُبقي الصفحات المختلطة ونحسّن تنسيق المزج فقط (كما فعل D-24)، أم نشطر المحتوى؟ ج: الشطر — موقع التوثيق يشحن نسختين كاملتين غير ممزوجتين. فالقارئ لا يرى الخطّين متشابكين في النثر الجاري أبدًا: النسخة الإنجليزية نثر إنجليزي وأسماء شاشات إنجليزية وعناوين إنجليزية، والنسخة العربية عربية بالكامل. والمعرّفات التقنية ليست نثرًا صراحةً — فمسارات الملفات، ومسارات التوجيه، وأفعال HTTP، وقيم التعدادات، ومتغيرات البيئة، وأسماء الحزم، ورموز المعرّفات تبقى لاتينية في النسختين، ملفوفةً بعلامات الكود كي تُعرض <code dir="ltr">، ويمنعها عزل lib/rehype-arabic.ts القائم من كسر الفقرة. واستثناء واحد مقصود: صفحة مسرد جديدة تحمل مطابقة أسماء الشاشات بين العربية والإنجليزية في عمودَي جدول — أعمدة لا جُمَل — وهذا بالضبط ما يُغني كلتا النسختين عن الترجمات الداخلية. أما آليًا: اللغتان ["ar", "en"] مع ar افتراضيةً (بما يطابق D-08)، وتنتقل المسارات إلى app/[locale]/page.tsx وapp/[locale]/[slug]/page.tsx مع الضرب الديكارتي الكامل للغة × المُعرِّف في generateStaticParams وdynamicParams = false، فيبقى الموقع ساكنًا بالكامل؛ وapp/page.tsx يعيد التوجيه دائمًا إلى /ar. والمصادر الإنجليزية تحتفظ بمساراتها، وتعيش المرايا العربية في مجلد شقيق ar/ (docs-content/ar/…، وspecs/ar/02-api.md، وapps/docs/content/ar/…)؛ ومواصفات الهندسة عدا مواصفة واجهة البرمجة تبقى إنجليزية فقط وخارج النطاق. وبلا أي اعتمادية تشغيل جديدة — لا تُضاف next-intl إلى apps/docs؛ فنصوص الهيكل تعيش في قاموس مُنمَّط apps/docs/lib/dictionary.ts يجعل المفتاح الناقص خطأً في الأنواع، ويتنقّل قرص LocaleSwitcher إلى المُعرِّف نفسه في اللغة الأخرى مع الحفاظ على الجزء التالي للمِرساة. ويحصل البحث على مسار فهرس لكل لغة بالطيّ العربي نفسه الذي تستعمله CommandPalette في الويب، وتُصدر البيانات الوصفية وخريطة الموقع alternates.languages للنسختين مع x-default/ar. وبوابتان تُبقيان الأمر أمينًا: اختبار الدخان يتحقق أن مصدر كل لغة × مُعرِّف موجود وغير فارغ وأن عدد عناوين ## فيه يطابق نظيره (وهذا ما يمسك الترجمة الناقصة)، واختبار جديد apps/docs/test/no-mixing.test.ts يتحقق أن لا محارف عربية في النثر الإنجليزي الجاري، ولا كلمات لاتينية من ثلاثة أحرف فأكثر في النثر العربي الجاري، خارج مقاطع الكود والروابط وقائمة سماح صغيرة لأسماء العلامات. المواصفة: specs/08-docs-bilingual.md.

D-29 — متى تكون «ثنائية اللغة» منتهيةً فعلًا؟

س: أُعلن تطبيق الويب ثنائي اللغة منذ D-08، ومع ذلك لم تصل حفنة من النصوص الظاهرة للمستخدم إلى ملفات الرسائل — قائمة محافظات مثبَّتة في الكود، وعلامة اسمية عربية فقط، ونص علامة إنجليزي ما يزال يحمل عربية. فهل نكنسها يدويًا كلما لاحظها أحد، أم نجعل البناء يرفضها؟ ج: كلاهما — جولة إتمام واحدة، ثم بوابة فحص تمنع التسريبات من العودة. والجولة تُغلق خمسة منها. (1) مُرشِّح المحافظات في apps/web/src/components/guardian/SpecialistsDirectory.tsx ينقل قائمته المثبَّتة إلى guardian.specialists.filters.governorates، وهو كائن مرتَّب مفاتيحه معرّفات لاتينية ثابتة (cairo، giza، alexandria، gharbia، dakahlia، assiut) كي تبقى القيمة المرسَلة إلى واجهة البرمجة ثابتة وتُترجَم التسمية وحدها — وإن كان عقد واجهة البرمجة يتوقع النص العربي، بقي العربي قيمةً وتُرجمت التسمية وحدها؛ وفي الحالتين يُتحقَّق من الاختيار مقابل apps/api أولًا، لأن سلوك التصفية يجب ألا يتراجع. (2) يتوقف SpecialistProfileSelf.tsx عن وصل المهارات بالفاصلة العربية دون شرط: يصلها بـ"، " تحت ar وبـ", " تحت en، ويظل يفصلها على ، و, معًا كي تدور البيانات المُدخلة بأي لغة دورةً كاملة. (3) يعرض components/common/Logo.tsx العلامة الاسمية العربية تحت ar وكلمة Nutqi تحت en، ولكلٍّ سمة lang المطابقة. (4) يفقد common.meta.brand شطره العربي في الإنجليزية — فتصير en هي "Nutqi"، وتبقى ar هي نُطقي — كي لا يحمل قالب العنوان الإنجليزي أي عربية. (5) تُضاف مفاتيح common.theme.* التي جاء بها D-27 إلى اللغتين. أما البوابة فهي apps/web/scripts/check-i18n.mjs الموصولة بمهمة lint في الويب، وتفشل حين تختلف مجموعتا مفاتيح ar وen في أي مساحة أسماء، أو حين تكون أي قيمة نصًا فارغًا، أو حين تحتوي أي قيمة في en على محارف عربية (مع سماح لـcommon.appName ولنصوص العلامة الثنائية اللغة عن قصد)، أو حين تطابق قيمةٌ في ar نظيرتَها في en مطابقةً حرفيةً وطولها أكثر من حرفين — وهو فخّ النسخ واللصق، مع سماح لأسماء المنتجات اللاتينية والأرقام. والعربية داخل تعليقات ملفات .tsx مقبولة وتبقى: فالبوابة لا تفحص إلا كائنات الرسائل النصية والنصوص الحرفية التي تصل إلى الصفحة. المواصفة: قسم جولة الإتمام في specs/04-i18n.md.

D-30 — ماذا تعني «الجاهزية للإنتاج» أمنيًا هنا؟

س: طلب مالك المنتج أن تكون كل التدابير الأمنية المعتادة قائمةً ومغطّاةً بخط التسليم. فما الذي كان ناقصًا فعلًا؟ ج: ثلاث فئات من الثغرات، أُغلقت جميعًا. (١) في جانب المتصفح: كانت الترويسات الأساسية جيدة، لكن بلا سياسة أمن محتوى. صار التطبيق يشحن سياسةً نافذة تحمل قيمة تعمية طازجة لكل طلب، فلا يعمل من النصوص البرمجية إلا ما ضمنه الخادم نفسه، ولا تُمنح unsafe-eval بحال. وأُضيف معها HSTS، وترويسات عزل المصادر المتقاطعة، وحُذفت لافتة إطار العمل. (٢) في جانب الخادم: صارت الواجهة تتحقق من كل متن طلب تحققًا صارمًا وترفض الحقول غير المعروفة رفضًا قاطعًا، وتحدّ من حجم المتن، وتحدّ من المعدّل حسب عنوان الشبكة بميزانية أضيق بكثير على مسارَي تسجيل الدخول ورمز التأكيد (مكمِّلةً قفل الحساب القائم من G15). وتوثيق الواجهة مُعطَّل في الإنتاج ما لم يُفعَّل صراحةً. (٣) عند الإقلاع: كان إعدادان يفشلان فشلًا مفتوحًا — فغياب مفتاح التوقيع كان يرتدّ صامتًا إلى قيمة معروفة للعموم (والمفتاح نفسه يوقّع رموز إعادة تعيين كلمة السر، فالرمز المزوَّر يعني الاستيلاء على الحساب لا انتحال الهوية فحسب)، وقائمة المصادر المسموحة الفارغة كانت تعني «اعكس أي موقع يسأل، مع بيانات الاعتماد». وصار كلاهما يرفض الإقلاع في الإنتاج، فيتوقف النشر الخاطئ بصوتٍ عالٍ بدل أن يعمل بلا أمان.

وأُضيف مسبار GET /api/v1/health للوسيط العكسي ولفحص صحة الحاوية: فحص حياةٍ وفحص قاعدة بيانات حقيقي، وخالٍ عمدًا من أرقام الإصدارات وسلاسل الاتصال ونصوص الأخطاء.

D-31 — ماذا يفحص خط التسليم الآن؟

س: كانت مهمة تكامل واحدة تشغّل البوابات الأربع. أيكفي ذلك لوصفه بالمعياري؟ ج: لا — فقد كان يثبت أن الشيفرة تُصرَّف وأن الاختبارات تنجح، ولا شيء غير ذلك. صار الخط يشغّل سبع مهامّ متوازية: البوابات الأربع في مواجهة PostgreSQL حقيقية؛ ومسح الأسرار وتدقيق التبعيات وسياسة التراخيص؛ والتحليل الساكن؛ ومراجعة التبعيات على كل طلب دمج؛ وبناء حاوية يُصدر قائمة مكوّنات البرمجية ويفشل عند الثغرات العالية أو الحرجة؛ وفحص ترحيلات قاعدة البيانات الذي يلتقط انحراف المخطط قبل وصوله إلى أي بيئة؛ ونشر إنتاجي لا يعمل إلا على main بعد نجاح البوابات. وكل مهمة تعلن أدنى صلاحيات تحتاجها وتحمل مهلةً زمنية. أما النشرات التمهيدية فتأتي من تكامل Vercel مع المستودع، فلكل طلب دمج رابط يضغطه المراجع.

D-32 — أين تعمل الواجهة البرمجية في الإنتاج؟

س: موقعا الويب والتوثيق يُنشران على Vercel تلقائيًا. فماذا عن الواجهة البرمجية؟ ج: مضيف Docker خلف Nginx، لا منصة بلا خواديم — فالواجهة تمسك اتصالات قاعدة بيانات طويلة العمر، وتكتب الملفات المرفوعة على القرص، وتشغّل الترحيلات عند الإصدار، وكل ذلك يصادم نموذج «بلا خواديم». وتُشحن العدّة في deploy/: حزمة Compose (الواجهة وPostgreSQL، مع فحوص صحة وخطوة ترحيل تُنفَّذ مرةً واحدة)، وقالب TLS لـNginx، ونصوص التهيئة الأولى والنشر والتراجع، ودليل تشغيل يغطي النسخ الاحتياطي والاستعادة وتجديد الشهادات. ومنفذ الواجهة يرتبط بالمضيف المحلي وحده، وNginx هو السطح العام الوحيد.

ويستحق التسجيل اكتشافان خرجا من بنائها: كانت صورة الحاوية تشحن عدّة البناء كاملةً وتعمل بصلاحيات الجذر (وصارت صورة تشغيل نحيفة، بمستخدم غير مميّز، وأصغر بنحو ٣٠٪)، ولم يكن لسياق بناء Docker ملف تجاهل في جذر المستودع — فكان كل بناء يسحب ملف .env الخاص بالمطوّر إلى سياق الصورة. ولم يُنشر شيء من ذلك في أي مكان، وقد أُصلح.

D-33 — موقع التوثيق صار ثنائي اللغة. فماذا يحلّ بالروابط التي شُوركت سلفًا؟

س: انتقلت صفحات التوثيق من /user-guide إلى /ar/user-guide و/en/user-guide، وكانت الروابط قد أُرسلت إلى العميل بالفعل. ج: كل رابط مسطّح قديم صار يصدر إعادة توجيه دائمة إلى صفحته الإنجليزية — اللغة التي كانت تلك الروابط تخدمها حين أُرسلت، فلا يتبدّل لسان الرابط المُشارَك تحت قارئه صامتًا. وجذر الموقع يعيد التوجيه إلى العربية، بما يوافق انحياز المنتج إلى العربية أولًا، ومبدّل اللغة حاضر في كل صفحة.

D-34 — سطّح D-26 مسارات القراءة التي شملها المسح. فلماذا ظلّت خمس نقاط تُخرج خطأ ٥٠٠؟

س: أخرج التنقّل في التطبيق صفحات خطأ ٥٠٠ في لسان التقييمات، ومشغّل الخطط والمقاييس، وملفّ الأخصائي، فضلًا عن ملفّ وليّ الأمر الذي يظلّ يدور بلا نهاية. وكان D-26 قد حسم مَن يسطّح البيانات. ج: لأن D-26 طُبّق انطلاقًا من قائمة مكتوبة باليد، وكانت نقاط التدفّقات العميقة هي بقعتها العمياء. والأسوأ أن التمريرة السابقة تركت تسامحًا خلفها — مُعينًا اسمه unwrapItems مع عشرات من اتحادات الأنواع X[] | Paginated<X> وثلاثيّات Array.isArray(x) ? x : x.items — فصار بوسع النقطة أن تنحرف من مصفوفة إلى غلاف ولا يفشل شيء: يبتلع الحارس الانحراف فتعرض الشاشة فراغًا صامتًا. هذا التسامح هو سبب بقاء الانحراف إصدارًا كاملًا. صار الجرد الآن مبنيًّا من الشيفرة لا من الذاكرة: كل مسار تعرضه الواجهة البرمجية، مقابلًا بكل موضع نداء apiGet/apiPost/apiPatch/apiPut/apiDelete والنوع الذي يطلبه، مع مقارنة الحمولة الحيّة بذلك النوع. وكشف المسح ٣٢ مسار قراءة غير متطابق لا خمسة — منها نقطتان تناديهما البوابات ولم تكونا موجودتين أصلًا (GET /users/me/languages وGET /specialists/me/work-info، وكلتاهما ٤٠٤)، وقراءتان بنطاق «حسابي» تمرّران معرّف المستخدم حيث يلزم معرّف SpecialistProfile (التقييمات والمواعيد المتاحة، فارغتان أبدًا)، وشبكة مواعيد تُرجع نصوصًا مجرّدة حيث يقرأ المعالج {time, disabled} (فكان كل زرّ ميعاد يُعرض فارغًا)، وصفّ إشعارات يرسل titleEn حيث يقرأ الجرس title، وأربعة عشر مسار كتابة كانت الواجهة ترفض أجسام طلباتها رفضًا صريحًا تحت forbidNonWhitelisted.

الواجهة البرمجية هي التي تتكيّف، كما قرّر D-26: مُحوّل واحد لكل وحدة، يُطبَّق على كل مسار قراءة، ويُخرج بالضبط الشكل الذي تعلنه البوابة — بلا علاقات قاعدة بيانات، وبلا مفاتيح خارجية، وبلا حقول createdAt/updatedAt الإدارية، والتواريخ بصيغة YYYY-MM-DD، والصور روابط جاهزة للاستعمال، ورموز التعدادات (لا نصوصًا مترجمة سلفًا) كي تظلّ البوابات تحلّ التسميات عبر @nutqi/shared (G21). وحيث تختلف لغة الشاشة عن لغة العمود، ينطق الشكل بلغة الشاشة: فـWeekBlock هو {day,start,end,sessionType}، والشهادة {title,org,from,to}، وملاحظة الحالة كتلة text واحدة. وكانت ثلاثة أعمدة غائبة خلف حقول تحرّرها الواجهة بلا مخزَن لها البتّة، فأُنشئت (WorkSchedule.sessionType وDayOff.reason وSession.startTime وCenter.about) — فالحقل الذي تحرّره الواجهة ويُسقطه الخادم صامتًا هو العيب نفسه في ثوب آخر.

أربع حمولات تبقى غير مسطّحة عن قصد، كلٌّ منها مُعرَّفة بدقّة لا محروسة: GET /specialists وGET /bookings تحتفظان بـ{items,total,page,limit} لأنهما الشاشتان الوحيدتان ذواتا ترقيم صفحات حقيقي؛ وGET /payments/mine تبقى {items,totals} لأن السجلّ يعرض مجاميع المدفوع والمعلّق عبر السجلّ كلّه، وهي مجاميع لا تشتقّها صفحة من الصفوف؛ وGET /notifications تبقى {items,unreadCount} للسبب ذاته في شارة الجرس؛ وGET /bookings/upcoming تحتفظ بغلافها ذي المفتاح الواحد {booking} كي يكون «لا حجز قادم» قيمةً لا جسمًا فارغًا. أما GET /specialists/:id/profile فتبقى مركّبة — شاشة واحدة بأقسام مسمّاة تقابلها حمولة واحدة بأقسام مسمّاة — غير أن كل قسم صار مسطّحًا ومُعلنًا بالكامل، بلا تخمين بسلاسل الوصول الاختياري عند موضع النداء.

أما الضمانات، فلأن سابقتها لم تكفِ: يتحقّق contract-deep-flow.e2e.spec.ts من مجموعة المفاتيح بالضبط لكل نقطة في النطاق (فالمفتاح الزائد يُفشل الاختبار لا الناقص وحده) ومن ألّا تركب معها علاقة؛ ويسجّل التشغيل نفسه الحمولات الحقيقية في apps/web/src/test/fixtures/api-payloads.json، ويعيد كل تشغيل لاحق التحقّق من الملف في مواجهة الخادم الحيّ، فلا يبلى الملف؛ وكل مكوّن كان ينهار صار يُركَّب في deep-flow-contracts.test.tsx في مواجهة تلك الحمولة المسجَّلة. وحُذف unwrapItems، ولم يبقَ في apps/web اتحادُ «مصفوفة أو غلاف». وأخيرًا كفّ حاجز الأخطاء عن ابتلاع الدليل: صار في بيئة التطوير يطبع الخطأ المرميّ ومعه أثر النداءات الأخيرة للواجهة البرمجية بأشكالها المرصودة، فتُسمّى النقطة التي أرسلت الحمولة الخاطئة على الشاشة بدل تخمينها. المواصفة: specs/10-deep-flow-contracts-i18n.md §A.

D-35 — تبيّن أن "دعم الإنجليزية" ليس مسألة ترجمة. فما الذي كان ناقصًا؟

س: طلب مالك المنتج "إضافة دعم الإنجليزية لكل المسارات". وكانت ملفات الترجمة متطابقة المفاتيح بالكامل أصلًا، وثمة قاعدة فحص تفرض ذلك. فأين كان النقص إذًا؟ ج: كان النقص في الشيفرة لا في ملفات الترجمة. أربعة أنواع، عولجت جميعًا. (١) الأسماء: كل شاشة تعرض اسم شخص كانت تُظهر الصيغة العربية أيًّا كانت لغة الواجهة — الشريط العلوي، والترحيب، وقوائم المرضى، وملخّص الحجز. صار الاسم يُحسم حسب لغة القارئ، ودليل الأخصائيين صار يرسل الصيغتين بدل صيغة واحدة يختارها الخادم (فالخادم لا يعرف لغة القارئ، واختياره كان يثبّت الموقع الإنجليزي على أسماء عربية). (٢) تسميات مكرّرة: أسماء أيام الأسبوع والأدوار والألقاب المهنية كانت منسوخة داخل ملفات الترجمة بجوار الخرائط ثنائية اللغة المشتركة، والنسخة لم تغطِّ كل القيم — فكان دور صاحب المركز يظهر بالرمز CENTER_OWNER. حُذفت النسخ، وبقيت الخرائط المشتركة المصدر الوحيد (G21). (٣) الترقيم والقوائم: كانت قوائم الأيام والمهارات تُوصَل بفاصلة عربية حتى في الإنجليزية. (٤) وأكثرها إعاقةً للمستخدم: كان تبديل اللغة يُسقط معاملات الرابط، فتضيع حالة التحقّق واستعادة كلمة السر وروابط "أكمل من حيث انتهيت" في منتصف المسار، ولم يكن إتمامها بالإنجليزية ممكنًا أصلًا.

وحمايةً لذلك: صارت قواعد الفحص ومجموعة الاختبارات تشتركان في وحدة قواعد واحدة، فلا يمكن أن تتناقضا. يسقط اختبار عند بقاء أي نص عربي مكتوب داخل مكوّن، وآخر يسمّي كل مفتاح موجود في لغة وغائب عن الأخرى، واختبار عرض لكل بوابة يشغّل شاشات حقيقية بالإنجليزية ويسقط إذا تسرّب أي حرف عربي إلى المخرجات — بما في ذلك داخل aria-label وplaceholder وtitle.

D-36D-35 جعل الأسماء تتبع اللغة. فلماذا ظلّت لوحة ولي الأمر الإنجليزية تحيّيك بالعربية؟

Q: بعد D-35 صار دليل الأخصائيين يرسل الصيغتين ويحلّهما البوابة حسب اللغة. ومع ذلك ظلّت لوحة ولي الأمر بالإنجليزية تعلن الجلسة القادمة باسم الأخصائية العربي، ومثلها جدول الحجوزات ودفتر المدفوعات وأسماء كاتبي التقييمات وكل صفوف الموارد البشرية. فما الذي بقي؟ A: D-35 أصلح الحمولتين اللتين كان ينظر إليهما، ولم تُطبَّق القاعدة على الإحدى والعشرين الأخرى. كل حمولة متبقّية تذكر شخصًا كانت ما تزال ترسل نصًّا واحدًا مُركَّبًا مسبقًا — specialistName وpatientName وapplicantName وguardianName وstaffName وauthorName وname في كشف الموظفين وفي قائمة الحظر — يُبنى في الخادم بالصيغة fullNameAr || "firstName lastName". والخادم لا يعرف لغة القارئ، فكان ذلك الاختيار يثبّت الموقع الإنجليزي على أسماء عربية في كل موضع اتُّخذ فيه. اختفت كلها. صارت الحمولة ترسل ما لديها من صيغ وتختار البوابة: personName(row, locale) حين يكون الصفّ هو الشخص، وrefName(row, "specialist", locale) حين يكتفي بالإشارة إليه.

الاصطلاح: قاعدة واحدة مطبَّقة في كل مكان. تبقى العلاقات محذوفة (D-26)، فالشخص المُشار إليه أربعة مفاتيح مسطّحة تسبقها تسمية الإشارة — specialistFirstName وspecialistLastName وspecialistFullNameAr وspecialistFullNameEn — والصفّ الذي موضوعه شخص يحمل المفاتيح الأربعة نفسها بلا بادئة، تمامًا كما كان Patient وبطاقة الدليل. ولا شيء آخر يتغيّر شكله: لا كائن متداخل {specialist: {...}}، ولا مجموعة مفاتيح ثانية تُحفظ. جانب الواجهة الخلفية دالّة واحدة (personRef(prefix, person) مع قائمة الأعمدة personNameSelect) يستعملها كل مُحوِّل؛ وجانب البوابة refName، مُهايئ من أربعة أسطر فوق personName القائمة. المتغيّر: الحجوزات (المريض والأخصائي)، ودفتر المدفوعات (كلاهما)، والجلسات، وبطاقات الخطط والمقاييس، والتقييمات، وطلبات التوظيف، وقائمة مرضى المركز (وليّ الأمر)، وكشف موظفي المركز، وقائمة الحظر، وجداول الموارد البشرية الخمسة. والموضع الوحيد الذي ما زال الخادم يركّب فيه اسمًا هو صفّ الإشعار، لأنه يخزّن نصًّا عربيًّا جاهزًا ونصًّا إنجليزيًّا جاهزًا — فصار كلٌّ منهما يُركَّب بلغته.

البدائل، لأن البيانات غير متجانسة. بعض الحسابات يحمل الصيغتين، وبعضها العربية وحدها، وبعضها الإنجليزية وحدها، وبعضها لا يحمل أيًّا منهما. تُفضّل personName صيغة اللغة النشطة، ثم تعود إلى مدخل التسجيل الخام firstName lastName، ثم إلى الصيغة الأخرى — فلا يظهر اسم فارغ أبدًا، ولا يخترع الخادم اسمًا نيابةً عن أحد. وحالات الأربع كلها مزروعة في ملف العقد المسجَّل حتى تمرّ بها اختبارات العرض فعلًا.

الحمايات: يتحقّق contract-deep-flow.e2e.spec.ts من مجموعة المفاتيح الدقيقة لكل حمولة متأثّرة (والمفتاح الزائد يُسقط الاختبار كذلك)، ويضيف تحقّقًا صنفيًّا يمرّ على كل حمولة مسجَّلة ويسقط عند أي مفتاح ينتهي بـName وليس عيادة أو فرعًا أو ملفًّا، وعند أي شخص يُنقل بأقل من مفاتيحه الأربعة. وسبع حمولات كانت ترجع فارغة في هذا الحصاد — مرضى المركز، وقائمتا طلبات التوظيف، وطلبات الموارد البشرية والحضور، وقائمة الحظر، وجلسات المريض — صارت تُزرع بصفّ لكلٍّ منها، فتُفحص أشكالها بدل افتراضها. وعلى جانب الويب يشغّل اختبار جدول الحجوزات على الحمولة المسجَّلة ويتحقّق من ظهور الصيغة الإنجليزية تحت en والعربية تحت ar، كما صارت اختبارات العرض "لا تسرّب عربيًّا في الإنجليزية" تحمل صيغًا عربية في بياناتها، فيسقط فيها أي مكوّن يحلّ الاسم باللغة الخطأ. المواصفة: specs/10-deep-flow-contracts-i18n.md §A2 و§B.

D-37 — هل كان النشر التلقائي على Vercel معطلًا؟

س: بدا الإنتاج عالقًا عند إصدار قديم مدة من الوقت، وصار النشر يُدفع يدويًا للّحاق. فهل توقّف تكامل GitHub؟ ج: لا — كانت طابورًا لا عطلًا. سجلّات النشر تفرّق بين الحالتين: النشر الناتج عن دفعة يسجّل لحظة الدفع إلى المستودع، أما النشر المُرسَل من الجهاز فيسجّل الأداة التي أرسلته. وكل دفعة في هذه الجلسة بدأت البناء خلال خمس ثوانٍ تقريبًا. والاستثناء الوحيد إصدار انتظر أربعًا وعشرين دقيقة، وسببه ظاهر في القائمة نفسها: كانت عشرة فروع لتحديث الاعتماديات قد فُتحت للتو، وكل فرع يحجز بناء معاينة على كلا المشروعين، فأشبع الطابور. وقد استُنتج الخطأ («التكامل ميت») من ثلاث عشرة دقيقة من الاستطلاع، ثم لم تزد عمليات النشر اليدوية على تكرار بناءٍ كان قادمًا أصلًا.

تغييران حتى لا يبتلع الطابور نشرًا حقيقيًا مرة أخرى: فروع تحديث الاعتماديات لم تعد تنتج نشرات معاينة (تُراجَع بذاتها، وخط الفحص الخاص بها يعمل كما هو)، وتطبيق الويب يتخطّى بناءه كليًا حين لا تمسّه الدفعة ولا تمسّ ما يعتمد عليه. أما موقع التوثيق فيظل يُبنى مع كل دفعة عن قصد — فهو يقرأ ملفات markdown من جذر المستودع وقت البناء، وهذه علاقة لا يراها مخطّط الاعتماديات الذي يستند إليه فحص التخطّي، فالتخطّي هناك قد ينشر توثيقًا قديمًا في صمت.

D-38 — شاشة «المرضى» عند الأخصائي كانت فاضية على طول. أنهي مسار المفروض يملاها؟

س: مالك المنتج طلب حاجة بديهية: «الأخصائي لازم يشوف المرضى اللي عنده حجوزات معاهم.» الشاشة موجودة أصلًا ومربوطة في القائمة الجانبية، لكنها بتنادي GET /patients — والمسار ده بيرجّع مرضى أسرة صاحب الحساب نفسه — يعني بيرجّع مصفوفة فاضية لأي أخصائي مهما كان سجل حجوزاته مليان. نضيف تفريعًا حسب الدور جوّه GET /patients، ولا نعطي بوابة الأخصائي مسارها الخاص؟ ج: مسار مخصّص GET /specialists/me/patients. التفريع حسب الدور كان هيخلّي مسارًا واحدًا يعني حاجتين مختلفتين حسب اللي بيسأل، والشاشتان مش شاشة واحدة أصلًا: قائمة وليّ الأمر سجلّ أسرة (أولاده، وصف المريض نفسه، والرقم القومي اللي أدخله عند التسجيل)، أما قائمة الأخصائي فهي حِمل حالات — مين المريض، ومين اللي بيجيبه، والعلاج ماشي إزاي. وجوهر D-26 إن كل حمولة لها شكل واحد تقدر الواجهة تصرّح به؛ وحمولة يتغيّر طقم مفاتيحها حسب دور المنادي هي بالضبط الانحراف اللي وُجدت حواجز تلك المواصفة عشان تمسكه. فـ GET /patients ما اتغيّرش ولسه معناه «مرضى أسرتي»، وGET /centers/me/patients لسه معناه «مرضى المركز ده»، والمسار الجديد معناه «المرضى اللي بعالجهم». تلات جُمَل، تلات مسارات.

النطاق: الحجز — بأي حالة — هو المفتاح الوحيد. القائمة مبنية من الحجوزات اللي specialistId بتاعها هو ملف المنادي نفسه: قيد الانتظار، أو منتظرة، أو مؤجَّلة، أو تمّت، أو ملغاة. الحجز الملغي يعني إن الأخصائي استلم بيانات المريض بالفعل، فإخفاء الصف بعد كده تمثيل مش خصوصية. ومفيش حاجة تانية بتمنح صفًا: لا «مرضى مركزي»، ولا «المرضى اللي قيّموني»، ولا بحث حر في جدول المرضى.

الصف نظرة علاجية، مسطّحة ومُصرَّح بها بالكامل (D-26 وD-34): id، ومفاتيح الاسم الأربعة بدون بادئة، وavatarUrl، وbirthDate، وgender، ووليّ الأمر في أربعة مفاتيح ببادئة guardianFirstName وguardianLastName وguardianFullNameAr وguardianFullNameEn (D-36 — الخادم ملوش لغة، فبيبعت الهجاءات اللي عنده والواجهة هي اللي تختار)، وبعدها علاقة العلاج: bookingsCount وlastSessionDate وnextBookingDate وdiagnosisCompleted. ستة عشر مفتاحًا، ولا واحد زيادة. وتلات حاجات مش موجودة عن عمد: nationalId (مُعرِّف أقوى مما تحتاجه شاشة حِمل الحالات)، وguardianPhone (المعالج محتاج يعرف مين بيجيب الطفل، مش دفتر تليفونات العيلة — وقائمة المركز محتفظة بيه لأن شغل السكرتارية إنها تتصل بالناس)، وisSelf (مفهوم أسري ملوش معنى في حِمل الحالات). وكل عدّاد وكل تاريخ محسوب من حجوزات وجلسات الأخصائي المنادي هو بنفسه، فلو معالجان بيتابعا نفس الطفل شافا صفّين مختلفين وما عرفش أي منهما حاجة عن جدول التاني.

الأمان هو النص اللي أخد الاختبارات. الوصول بالمعرّف المباشر كان مضبوطًا أصلًا — PatientsService.assertAccess بيدّي الأخصائي قراءة فقط لما يكون في حجز رابط بينهم، وGET /patients/:id ومستنداته وجلساته كلها بتعدي منه — بس «مضبوط أصلًا» دعوى مش ضمان، فبقى مثبَّتًا في specialist-patients.e2e.spec.ts: تلات أخصائيين، اتنين منهم محجوزين عن عمد مع نفس الطفل. الأخصائي «أ» بيشوف مريضيه هو بالظبط، ومريض الأخصائي «ب» لا في القائمة ولا بالمعرّف المباشر ولا المستندات ولا الجلسات (403)؛ و«ب» بيتمنع بنفس الطريقة عن مريض «أ» اللي حجزه ملغي؛ وأخصائي بدون أي حجوزات بيستقبل قائمة فاضية بدل خطأ ومبيوصلش لأي مريض بالمعرّف؛ وقراءة مش بتتحوّل لكتابة (PATCH /patients/:id لسه بيرجع 403)؛ ووليّ الأمر ممنوع من المسار كله؛ وصف «أ» للطفل المشترك بيبيّن حجوزات «أ» التلاتة وتاريخ «أ» الجاي وتاريخ جلسة «أ» — مش أربعة «ب» ولا تاريخه الأقرب ولا جلسته الأحدث.

وشاشة ملف المريض اتصالحت مع العقد بدل ما تخمّن. PatientProfileView مشتركة بين بوابتي الأخصائي والمركز وبتقرأ GET /patients/:id المحمي أصلًا، فهي محتفظة بالمسار ده. اللي اتغيّر هو اللي بتقراه منه: صف الرقم القومي راح — مش جزء مما تقول المواصفة 11 إن الأخصائي يشوفه — وحلّت مكانه حالة استمارة التشخيص، وهي معلومة المعالج بيتصرّف بناءً عليها. والنظرة العامة كمان بطّلت تستخدم السلاسل الاختيارية للوصول لحقول قد تكون غير موجودة: patient بيتضيّق نوعه مرة واحدة في الأول، فكل حقل تحته بيقرأ مفتاحًا مُصرَّحًا في العقد، ولو المريض مش موجود بتظهر حالة فراغ صريحة بدل خمس شرطات شكلها بيانات حقيقية. والحواجز كالعادة: طقم المفاتيح مؤكَّد بالظبط في contract-deep-flow.e2e.spec.ts، والحمولة المسجَّلة بتتعاد على مكوّن القائمة تحت ar وen، واختبار عرض البوابة الإنجليزية بيركّب الشاشة على مسارها الجديد. المواصفة: specs/11-specialist-patient-scope.md.

D-39 — بناء الفريق السابق لسه شغّال على الإنترنت. ناخد منه إيه، ونرفض إيه؟

س: نُطقي اتبنى مرة قبل كده، ونُصّي البناء ده لسه منشورين وعامّين: واجهة برمجية على Django REST Framework بتنشر مخططها بنفسها بصيغة OpenAPI 3.0.3 (‏40 مسارًا، و87 عملية، و103 مخططات)، وعميل ويب على Next.js. قراءة شغل حد تاني رخيصة؛ السؤال هو نعمل بيها إيه. نعتبره أثرًا قديمًا يتشال بالكامل، ولا نعدّي عليه بندًا بندًا وناخد اللي أحسن من اللي عندنا؟ ج: بندًا بندًا — وأربعة من بنودهم أحسن من اللي عندنا. المقارنة اتكتبت صفحةً موجَّهة للعميل (docs-content/ar/07-legacy-comparison.md، والمُعرّف legacy-comparison، في النسختين) مش مذكرة داخلية، لأن العميل دفع في البناء ده كمان ومن حقه حساب واقعي بما بيحسنه. وهو بيحسن حاجات كتير: سطح المصادقة مكتمل ومقفول صح (دخول برمز حامل مع مسارات صريحة للدخول والتجديد والتحقق والخروج، وطقم استعادة كامل برمز OTP، و401 على المسارات الشخصية، وبيانات مرجعية عامة)، والمؤهلات المهنية مُنمذَجة جداولَ مش مكتوبة نصًا، وأنواع الحسابات مخططات صريحة، وكل قائمة مُصفَّحة بتشارك اتفاق استعلام واحد.

المُتبنّى (أربعة). (1) بيانات أماكن مرجعية مُطبَّعة بالاسمين — دولة ومحافظة ومدينة جداول، وكل صف شايل اسمًا عربيًا واسمًا إنجليزيًا. إحنا بنخزّن المحافظة والمدينة نصًا حرًا، وده بالظبط سبب إن مدينة متكتوبة بالعربي بتتعرض بالعربي على الموقع الإنجليزي، وسبب إن ترشيح الدليل مطابقة نصوص؛ والجدول المرجعي بيحل الاتنين مرة واحدة. (2) تصنيف تخصصات مُطبَّع — تخصص وتخصص فرعي، ثنائيا اللغة، ككيانين بيشير لهما سجل العمل. اللي عندنا نص حر معبور لأنواع الجلسات الأربعة عبر خريطة التسميات المشتركة، وهي الآلية اللي وثّقها D-26 للتسطيح: الجسر شغال، بس مش قادر يعبّر عن تخصص فرعي ولا يتوسّع من غير تعديل شيفرة. ده حل التفافي مش نموذج. (3) التحقق من توافر البريد والتليفون قبل الإرسال — عندهم السؤال بيتسأل على الخطوة اللي جمعت المعرّف؛ وعندنا التكرار بيتكشف بـ409 بعد ما الاستمارة كلها تتملّي. وفي معالج من أربع خطوات ده الطرف الغلط من الرحلة. (4) search وordering عامّين على القوائم المُصفَّحة — رخاص، وبيتركّبوا مع المرشِّحات الموجودة، وبيخلّوا الشاشة تضيف أداة ترتيب من غير تغيير في الواجهة البرمجية. وفي بندان مسجَّلان مش مجدولين: سعر أول جلسة لكل تخصص (طبيعي بعد ما جدول التخصصات يتعمل)، والمصادقة برمز Bearer جنب جلسة الكوكيز لعميل محمول في المستقبل — إضافة مش استبدال.

المرفوض (اتنين)، لأن الاتنين بيتعارضوا مع قرارات وراها اختبارات أصلًا. غلاف التصفيح بتاعهم على كل قائمة متسق، وللاتساق قيمة — بس D-34 هو سبب إننا بنرجّع مصفوفات مسطّحة إلا لما الشاشة تُصفِّح فعلًا: التسامح القديم مع «مصفوفة أو غلاف» خلّى 32 مسار قراءة ينجرفوا في صمت، فتتعرض شاشات فاضية بدل ما تفشل، والغلاف الشامل بيرجّع نفس الالتباس ده. والأسماء المركَّبة مسبقًا عندهم هي اللي D-36 شالها من ثلاث وعشرين حمولة: الخادم ملوش لغة، فالاسم المُجمَّع على الخادم بيثبّت نسخةً على تهجئة النسخة التانية. الحمولات بتبعت التهجئات اللي عندها، والبوابة هي اللي بتختار.

وعلينا دَين واحد بأمانة. مخططهم العام وواجهة Swagger UI المستضافة فوقه ميزة حقيقية — هي اللي خلّت المقارنة دي دقيقة مش تقريبية، وهي اللي بيحتاجها أي مطوّر عايز يتكامل. واللي عندنا بيتولّد بس معطّل في الإنتاج افتراضيًا (ENABLE_API_DOCS، من D-30)، وده القرار الصح لـوحدة تحكم تفاعلية فوق واجهة إنتاج، والقرار الغلط لمستند المخطط نفسه. المتابعة: ننشر مواصفة مُنقّاة للقراءة فقط لمن يتكاملون، وتفضل الوحدة التفاعلية مطفية. ومسجَّل كمان تحذيرًا لا ممارسة: مخططهم المنشور لسه شايل العنوان والوصف ورقم الإصدار الافتراضية من المولّد — والمخطط مستند بيقرأه العملاء، والمفروض يتسمّى تسمية تليق بيه.