سجل التغييرات

GitHub

كل تغيير جدير بالذكر، إصدارًا بعد إصدار. الملف نفسه يأتي مع المستودع ومترجم إلى جميع اللغات الـ19.

[1.241.1] — 2026-10-07

التقييمات الحية تعمل من البداية إلى النهاية في كل لغة، وخريطة الوظائف تضع الإعلانات الطويلة متعددة المواقع، ولم يعد النشر يقتل مسحًا قيد التشغيل.

تم الإصلاح

  • التقييمات ترى الأسلوب كاملًا. كانت ميزانية السياق (144 كيلوبايت) ما تزال تقطع نهاية modes/oferta.md بعد تضمين السيرة الذاتية والملف الشخصي، فكان كل تقييم حي يفقد كتله الأخيرة. صارت الميزانية 192 كيلوبايت والحد المرن للموجّه 256 كيلوبايت، وهما معرَّفان مرة واحدة في llm-dispatch.mjs.
  • تقارير باللغة المختارة. صارت تعليمة اللغة تفتتح الموجّه كما تختمه، وتُكتب الكلمات العادية (الدور والفريق والتصميم…) بلغة التقرير؛ ولا يبقى بالإنجليزية سوى الأسماء والتقنيات والاقتباسات الحرفية. يعالج تقارير الهندية واليابانية التي كانت في معظمها بالإنجليزية.
  • التقييمات البطيئة لم تعد تنتهي برمز HTTP 502. مهلة التقييم 300 ثانية (كانت الافتراضية لدى المزوّد، 180 ثانية).
  • حروف الكتل بالهندية. يُتعرَّف الآن على تقرير يكتب الحروف A–G بالديفاناغاري (## ए) و## बी) … ## जी))، ويقبل SCORE: الأرقام المحلية (३.८ و٣٫٨ و3.8) والفاصلة العشرية.
  • خريطة الوظائف: المواقع الطويلة. كانت خلية مثل «Berlin Office · Berlin · Germany · Bosnia · …» (أكثر من 300 حرف) تُرفض برمز HTTP 400 فلا يظهر للوظيفة مؤشر. صار يُستخدم أول مكان حقيقي (· و; و| و / وفواصل الأسطر؛ وتُتخطى مقاطع «Remote …» و«(HQ)» و«Office»)؛ ولا يُرفض إلا استعلام يتجاوز 2000 حرف.
  • النشر مقابل المسح كل ساعة. يوقف النشر مؤقّتي المسح والتقييم التلقائي، وينتظر انتهاء المسح الجاري (حتى 15 دقيقة)، ثم يعيد التشغيل ويستأنف المؤقّتين دائمًا، بما في ذلك عند التراجع.

أُضيف

  • خريطة الوظائف في cvstart.org بجميع اللغات الـ 17، وفي ملفَّي README بالإنجليزية والروسية.
  • لقطات الخريطة بكل اللغات: لقطة لكل لغة (scripts/capture-map-screenshots.mjs) في ملفات README السبعة عشر، وعلى cvstart.org، وفي قسم فرعي جديد في المساعدة بعنوان «كيفية استخدام الخريطة» (§33، ×17).

ملاحظات

  • الاختبارات 4404 ← 4419، واختبارات المتصفح 118. اجتاز فحص الجودة عن بُعد (كل المسارات والموقع والروابط وLLM الحي × 17 لغة) على الإنتاج.
  • غير مشمول في هذا الإصدار: التقييمات المتدفقة؛ تبقى مهلة 300 ثانية حدًا صارمًا (قد يبلغها نموذج محلي بطيء)، ويواصل جسر Hermes استدعاءه دون تدفق.

[1.241.0] — 2026-10-07

إصدار تقوية بعد مراجعة شيفرة أجراها 28 وكيلًا (نحو 190 ملاحظة مُتحقَّق منها): أُغلقت فئات الأمن وتعطّل العملية وفقدان البيانات، مع خريطة وظائف بمساهمة @bullitt186.

أُضيف

  • خريطة الوظائف (#/map) — وظائف خط الأنابيب وآخر مسح والمتتبّع على خريطة OpenStreetMap، بترميز جغرافي عبر Nominatim مع User-Agent مُعرِّف، وطابور بمعدل طلب واحد في الثانية، وتراجع تدريجي، وذاكرة مؤقتة على القرص. تُقدَّم Leaflet من هذا الخادم نفسه (دون CDN)؛ ولا تزيد CSP سوى أصل البلاطات في img-src. بمساهمة @bullitt186 (#381).
  • حارس الطلبات — يرفض الخادم قيمة Host لا يخدمها (DNS rebinding)، ويرفض الكتابات عبر المواقع وطلبات GET ذات الأثر الجانبي (/api/stream/* و/api/run/* و/api/geocode). والوكيل العكسي الذي يمرّر اسمًا عامًا يُدرجه في ALLOWED_HOSTS؛ ويكتب سير عمل النشر ملف systemd drop-in هذا انطلاقًا من الرابط العام.
  • بوابة التغطية — يطبع npm run test:coverage:gate (scripts/coverage-report.mjs) تغطية الأسطر والفروع لكل ملف، ويفشل دون خط أساس متصاعد.

تم الإصلاح

  • الطلب المشوّه لم يعد يوقف الخادم. كان Express 4 يتجاهل معالجات async المرفوضة، فكان حقل بنوع خاطئ في /api/deep أو /api/auto-pipeline أو /api/career-plan أو /api/tracker، أو بايت NUL في /api/stats، يُنهي العملية؛ وصارت هذه الآن ردود JSON برمز 400، وأي رفض آخر يصبح 500.
  • المسح المُجهَض أو الفاشل لم يعد يمحو آخر النتائج. يُحتفظ بـ last-scan.json عند إيقاف المسح أو فشل كل المصادر، ويدمج مسح الشركة الواحدة بدل أن يستبدل. وإدخال واحد مضبوط خطأً في portals.yml لم يعد يُجهض المسح كله.
  • التقييمات ترى الأسلوب كاملًا. كانت ملفات الأوضاع تُقطع عند 16 كيلوبايت، فخسر oferta.md (92 كيلوبايت) الكتل C–G؛ وصارت الحدود لكل ملف 128 كيلوبايت للأوضاع و64 كيلوبايت للسيرة الذاتية، ويُبلَّغ عن أي قطع. والإجابات الفارغة أو المقطوعة تفشل بدل كتابة تقرير فارغ.
  • SSRF: تُفحص قفزات إعادة التوجيه من جديد بحثًا عن وجهات خاصة، وتُفحص كل إجابة DNS، ويشمل أجل المسح الجسم، ولا تذهب مفاتيح *_BASE_URL إلا إلى مضيف المورّد (Hermes/Ollama: الحلقة المحلية أو الشبكة المحلية)، ويعمل pandoc مع --sandbox.
  • توقفات حلقة الأحداث: استُبدل تعبيران نمطيان تربيعيان (تجريد HTML وتنقية markdown) بماسحين خطيين.
  • فقدان البيانات: كان حذف صف من خط الأنابيب يُبقي تعويض الصفوف الأخرى؛ وكان $& في رابط يُفسد pipeline.md؛ وكانت عمليات حفظ المقابلات التجريبية والتواصل والبحث المعمّق تكتب فوق ملفات سابقة؛ وكان سجل المقابلات يحتفظ بأول 40 دورة بدل آخرها.
  • طبقة LLM: يحترم خط الأنابيب التلقائي والبحث المعمّق LLM_PROVIDER وكل مزوّد عبر التوزيع المشترك، ويسجّلان الاستخدام ويبلّغان عن القطع؛ ولم يعد تقييم/اختبار Gemini يحفظ تقريرًا؛ ويستخدم OpenAI المعامل max_completion_tokens، ويُرسَل OPENAI_MODEL الخاص بـ Codex فقط على أنه gpt-5؛ ويُحاط وصف الوظيفة بسياج بوصفه بيانات غير موثوقة.
  • ملفات PDF للتقرير والبحث المعمّق والمضمَّنة تمرّر --skip-fact-check (كان التحقق من وقائع السيرة الذاتية يحجب أي تقرير فيه أرقام)؛ ولم يعد تنسيق صفحة الملف الشخصي يُتجاوَز.
  • الماسحات: يُفهرس الحجر الصحي بالرابط (فتصحيح رابط يعيد المحاولة)، وإزالة التكرار داخل التشغيل الواحد بالرابط القانوني، ولم تعد مدخلات القوائم الفارغة تطابق كل شيء، والكتابة ذرّية، وصفوف السجل skipped_* ليست «مرئية».
  • التحكم في العمليات: لم يكن تصعيد SIGKILL يعمل أبدًا؛ وتقتل التشغيلات الدفعية مجموعة عملياتها كاملة عند قطع الاتصال.
  • CI والإصدار: يشغّل CI الآن بوابات تكافؤ سجل التغييرات وسير العمل وi18n؛ وأذونات سير العمل بأقل الامتيازات؛ وأسرار الإنتاج فقط ضمن بيئة production؛ وحزمة npm تشحن الآن docs/help/.
  • الاختبارات: لم يعد أي اختبار يلمس الشبكة أو المشروع الأب الحقيقي (يُحترم دائمًا CAREER_OPS_ROOT الصريح)؛ واستُبدلت الخوارزميات المنسوخة بشيفرة الإنتاج.

ملاحظات

  • الاختبارات 4056 ← 4404، واختبارات المتصفح 118. خط أساس تغطية الخادم: الأسطر 96.7 %، الفروع 86.3 % (scripts/coverage-baseline.json).
  • صارت المواصفات الآن في مجلد واحد، docs/sdd/specs/؛ وخطة البرنامج في docs/sdd/PLAN.md، ومصفوفة الثابت ← البوابة في docs/sdd/TRACEABILITY.md.
  • ملاحظة للمشغّل: خلف وكيل عكسي اضبط ALLOWED_HOSTS=<المضيف العام>؛ وصار عنوان أساس Ollama/Hermes على مضيف عام مرفوضًا الآن.

[1.240.0] — 2026-10-06

تكافؤ مع المشروع الأب — career-ops main @ 62905981 (VERSION 1.35.0، و177 التزامًا في المنبع منذ b39931e). ستة مصادر جديدة — 103 ← 109 (104 EN + 5 RU) — وإصلاحات منقولة إلى ثمانية مزوّدين قائمين، وصيغة قائمة المهام لخط الأنابيب، بمساهمة @bullitt186.

أُضيف

  • ستة مصادر جديدة — 103 ← 109 (104 EN + 5 RU)، والمحوّلات الإنجليزية 98 ← 104.
    • ADP Workforce Now (workforcenow.adp.com مع cid + ccId) — ترقيم الصفحات بـ $skip وفق الصفوف المُعادة فعلًا؛ والاستجابة التي لا تحوي jobRequisitions ولا meta تُلقي خطأً بدل أن تُقرأ لوحةً فارغة؛ وإثراء التفاصيل اختياري (adpWorkforcenow.fetchDetails، الافتراضي 25، والحد الأقصى 100).
    • Gupy (employability-portal.gupy.io) — مسح بالكلمات المفتاحية على مستوى اللوحات كلها، من الأحدث إلى الأقدم مع توقف مبكر عند since_days؛ ويُقرأ workplaceType كسلسلة مفردة بالبرتغالية البرازيلية كي لا تضيع الوظائف الهجينة؛ وتُسقَط صفحات التوظيف السرّية.
    • JazzHR (*.applytojob.com) — محلّل بطاقات مع احتياط للروابط المجرّدة، فيظل إعادة التصميم الجزئي يُخرج وظائف؛ وروابط الوظائف دون أي بطاقة مُحلَّلة تُلقي خطأً، أما اللوحة الفارغة حقًّا فتُعيد [].
    • Startup Jobs (startup.jobs/feeds/jobs) — RSS بطلب واحد؛ والمحتوى المشوّه أو HTML أو المبتور يُلقي خطأً بدل إعادة [] (إصلاح المراجعة اللاحقة للدمج في المشروع الأب، 08fe5d06)؛ ويُقنَّع CDATA والتعليقات قبل مطابقة العناصر.
    • Taleo (<tenant>.taleo.net، ويُرفض tre.taleo.net) — طلب GET للهيكل ثم POST إلى searchjobs؛ والهيكل بلا معرّف بوابة يُلقي خطأً على أنه لوحة خاصة؛ وتُطابَق الأعمدة بعناوينها.
    • UKG Pro / UltiPro (recruiting[N].ultipro.{com,ca}) — Top=50 + Skip، ولا يُوثق بـ totalCount شرطَ توقف أبدًا، وحد الصفحات 100 (ويُقيَّد عند 1500)، وتُحلَّل صفحات التفاصيل الاختيارية بمسح أقواس مدرك للسلاسل النصية.

تم الإصلاح

  • خط الأنابيب بصيغة قائمة المهام في المشروع الأب — يكتب career-ops في المنبع data/pipeline.md على هيئة قسمي ## Pending / ## Processed من صفوف - [ ] url | …، وكانت الواجهة تقرؤه على أنه 0 معلّقة وتكتب إليه بإلحاق كتلة مسيّجة في نهاية الملف، حيث لا ينظر المشروع الأب أبدًا. صارت parsePipeline وaddPipelineUrl وremovePipelineUrl تقرأ هذا الشكل وتكتب به (مع إزالة التكرار مقابل الصفوف المعالَجة أيضًا؛ والتعويض مقطعًا موسومًا note: comp …)؛ أما الصيغة ذات الكتل المسيّجة فلم تتغيّر. مساهمة من @bullitt186 (#380).
  • دمج البلد في الموقع — Ashby (address.postalAddress الأساسي) وBreezy (country.name) وRecruitee (location المسطّح) تُلحق البلد ما لم يحمله الاسم أصلًا ككلمة كاملة، فلم تعد وظيفة عن بُعد بلدها الأساسي المملكة المتحدة والثانوي الولايات المتحدة تُسقَط بمرشّح الموقع.
  • MokaHR — يُقبل المضيف الإقليمي hire-r1.mokahr.com (قائمة السماح هي بالضبط app.mokahr.com + hire-r1.mokahr.com)، ويستخدم طلب API POST وروابط الوظائف مضيف اللوحة نفسها، وصار success:false داخل الاستجابة يُلقي خطأً بدل أن يُقرأ لوحةً فارغة.
  • Avature — تُحلَّل بطاقات article--jobs (العنوان من رابط الترويسة، والموقع من عنصر span ذي الصنف icon-address).
  • SAP SuccessFactors — رابط البحث المحفوظ /go/<Category>/<id>[/<offset>]/ يُحَلّ إلى جذر المستأجر؛ وكان يصل إلى …/tile-search-results/ الذي يردّ بـ 200 دون أي بطاقة.
  • Deutsche Bahn — يُفرغ db.jobs أي صفحة نتائج تحوي وظيفة لا يستطيع عرضها؛ وصار المسح يُرتَّب وفق pubExternalDate_tdt بـ 1000 في الصفحة (5 صفحات كحد أقصى)، ويتوقف بحسب العدد الخام لروابط النتائج، ويُلقي خطأً عند غياب ترويسة النتائج أو تعذّر تحليل روابط الوظائف بدل إعادة لوحة ناقصة.
  • HigherEdJobs — كان تحدّي Incapsula المُجاب عنه برمز HTTP 200 يُحلَّل على أنه صفر عناصر فيُقرأ لوحةً فارغة؛ وصار يُلقي خطأً.
  • اختبار كان يقرأ المشروع الأب الحقيقي على جهاز المطوّر — ضبط tests/reports-list-cache.test.mjs المتغير CAREER_OPS_ROOT على مجلد مؤقت لا يحوي cv.md/portals.yml، وهو ما يتجاهله paths.mjs، فكان يعدّ محليًا تقارير المشروع الأب (6 بدل 3) بينما يمرّ في CI الذي لا مشروع أب بجانبه.

ملاحظات

  • الاختبارات 3782 ← 4056. حزم جديدة: sources-adp-workforcenow وsources-gupy وsources-jazzhr وsources-startup-jobs وsources-taleo وsources-ultipro وsources-location-country-fold وsources-deutschebahn-blanked-page وsources-higheredjobs-challenge-page، إضافةً إلى parsers-pipeline-checklist من #380.
  • المشروع الأب: أُوصل الفرع إلى main في المنبع (144 ← 0 التزامًا متأخرًا) مع التحقق من سلامة تباعدات ADR-0002 الأربعة؛ أما تراجع المنبع عن LOCATIONISH_RE السيريليّ فلم يُنقل.
  • شُحن سابقًا في v1.239.0 ولا يتكرر هنا: AppliTrack وNEOGOV وRed Rover وSchoolSpring وOCC Mundial ولوحات Greenhouse المضمَّنة وWorkday myworkdaysite.com.
  • غير المنقول، مع الأسباب: خروج الوكيل الموثوق (CAREER_OPS_TRUST_PROXY_EGRESS، و_http.mjs / _ip-guard.mjs / _dns-cache.mjs) — يفحص web-ui عبر ناقله الخاص http-json.mjs؛ وسجل أعمدة scan-history.tsv وإزالة تكرار طلبات التوظيف الواعية باللغة — يقرأ web-ui المواضع 1–12 التي لم تتحرك، ويُزيل التكرار بحسب الرابط؛ وrequisitionId / language في SmartRecruiters — لا مستهلك لهما في شكل الوظيفة لدى web-ui؛ وisoCountry في Gem ← اسم البلد — يحتاج جدول alpha-3 من 249 مدخلًا في المشروع الأب، وقد أُرجئ؛ والتغييرات الخاصة بواجهة الأوامر فقط (الأوضاع، والمحدّث، وdoctor، وحمولة PDF/ATS، وسكربتات المتتبّع، ولوحة Go، ووضع سنغافورة).

[1.239.4] — 2026-09-29

تصحيح ناتج عن اختبار الانحدار المباشر للإصدار v1.239.3.

تم الإصلاح

  • أفادت التقييمات الكورية والصينية التقليدية بغياب ملخص الدرجة. تبلغ هذه التقارير 10–11 ألف حرف ويأتي الملخص في آخرها، فكان يقع بعد حد 8192 رمزًا للمخرجات. صارت التقييمات (وخط المعالجة التلقائي) تسمح بـ 16384 رمزًا، وإذا قطع المزوّد الإجابة رغم ذلك يذكر أول تحذير ذلك بدل سرد الكتل الناقصة.
  • عدّ الفحص المباشر إجابة ألمانية قصيرة غير ألمانية (3 كلمات توقف ألمانية ولا إنجليزية). قد تحوي الإجابة القائمية القليل منها؛ ومع غياب الإنجليزية تمامًا تكفي الآن ثلاث.
  • الاختبارات 3778 ← 3782.

ملاحظات

  • تُبقي مسارات البحث المعمّق والأوضاع على حد 8192 رمزًا: إجاباتها لا تبلغه في العمليات المباشرة.

[1.239.3] — 2026-09-29

تصحيح ناتج عن اختبار الانحدار المباشر للإصدار v1.239.2.

تم الإصلاح

  • ظلت التقييمات العربية تفشل في فحص A–G بعد أن أصلح الإصدار v1.239.2 اليابانية والهندية. كشف هيكل مُقنَّع للعناوين (دون نص التقرير في السجل) السبب: التقرير العربي يكتب حرف الكتلة بالعربية بترتيب أبجد مهما طلب الموجّه. صار الفحص يقبل أ ب ج د هـ و ز على أنها A–G حين يأتي الحرف منفردًا قرب بداية العنوان، كما كان يقبل أشباه الحروف الكيريلية؛ ولا تُحتسب الحروف نفسها داخل كلمة عربية.
  • يطبع الفحص المباشر هذا الهيكل المُقنَّع عند فشل فحص A–G، ليمكن تشخيص الحالة التالية من السجل.
  • الاختبارات 3776 ← 3778.

ملاحظات

  • قالت ملاحظة الإصدار v1.239.2 إن الفحص لن يُخفَّف؛ أما للعربية فقد خُفِّف الآن، لأن ثلاث عمليات مباشرة أظهرت أن الموجّه وحده لا يحافظ على الحرف اللاتيني.

[1.239.2] — 2026-09-29

تصحيح ناتج عن اختبار الانحدار المباشر للإصدار v1.239.1.

تم الإصلاح

  • ترجم تقييم عربي أحرف الكتل وأسقط تقييم ياباني ملخص الدرجة. كان الإصدار v1.239.1 يكرر اللغة وحدها في السطر الأخير من الموجّه. صار هذا السطر يذكر أيضًا أن الحرف اللاتيني A–G في كل عنوان وكتلة ---SCORE_SUMMARY--- (العلامات والمفاتيح بالإنجليزية) لا تُترجم، ويطلب الكتابة بخط اللغة نفسها.
  • الاختبارات 3775 ← 3776.

ملاحظات

  • لم يُخفَّف فحص التقرير ليقبل أحرفًا أو علامات مترجمة: الموجّه هو ما يُبقي التنسيق ثابتًا.

[1.239.1] — 2026-09-29

تصحيح ناتج عن اختبار الانحدار المباشر للإصدار v1.239.0.

تم الإصلاح

  • كانت التقييمات باليابانية والأوكرانية تصل بالإنجليزية تقريبًا بالكامل. كان توجيه اللغة في أول الموجّه، تتبعه عشرات الكيلوبايتات من ملفات المشروع ووصف الوظيفة بالإنجليزية. صار الموجّه يعيد ذكر اللغة في سطره الأخير.
  • كان الفحص المباشر يعدّ المسح فاشلًا حين يكون الخادم ينفّذ مسحًا آخر (المسح الساعي الذي أُعيد مباشرة بعد النشر). رفض الخادم لمسح ثانٍ سلوك صحيح؛ صار الفحص ينتظر ويعيد المحاولة حتى 30 دقيقة.
  • الاختبارات 3774 ← 3775.

ملاحظات

  • لم يُخفَّف قيد المسح الواحد في الخادم: ما زال المسح الثاني يُرفض بالرمز SCAN_BUSY. تغيّر الفحص فقط.

[1.239.0] — 2026-09-29

تكافؤ مع career-ops @ b39931e: خمسة مصادر جديدة وثلاثة إصلاحات منقولة.

أُضيف

  • خمسة مصادر جديدة (98 ← 103؛ المحوّلات الإنجليزية 93 ← 98): AppliTrack (applitrack.com/<المنطقة>) وNEOGOV (SchoolJobs / GovernmentJobs) وRed Rover (jobs.redroverk12.com/org/<id>) وSchoolSpring (<المنطقة>.schoolspring.com) لتوظيف المدارس والقطاع العام، إضافةً إلى OCC Mundial أكبر موقع وظائف في المكسيك (provider: occ صريح مع استعلامات بحث). كلٌّ منها مثبَّت على مضيفه، ويستخدم HTTPS فقط، ويرفض إعادة التوجيه.
  • الاختبارات 3500 ← 3774.

تم الإصلاح

  • لوحات Greenhouse المضمَّنة (job-boards.greenhouse.io/embed/job_board?for=<اللوحة>) كانت تشير إلى لوحة اسمها “embed” فتفشل؛ صارت اللوحة تؤخذ الآن من ?for=، وعلى مضيف Greenhouse حقيقي فقط.
  • لوحات Workday على myworkdaysite.com (/recruiting/<tenant>/<site>) لم تكن تُتعرَّف؛ صارت تُفحص الآن، وتشير روابط الوظائف إلى المضيف نفسه.
  • اسم تقرير يحتوي أقواسًا (042-acme-(berlin)-….md) كان يُقطع في المتتبّع عند أول )؛ صار الاسم يُحفظ كاملًا الآن.

ملاحظات

  • تغييرات المشروع الأب الخاصة بواجهة الأوامر فقط لم تُنقل — قوالب PDF والسيرة الذاتية، وscaffolder، ومشغّل الدُّفعات، واستيراد career-profile، وتغطية الكلمات المفتاحية في ATS، وترجمات أوضاع المقابلة، ومضيف CLI pi: لا يشغّلها web-ui.
  • فتح مثل هذا التقرير من المتتبّع ما زال يتطلب أن يقبل مسار التقارير الأقواس في اسم الملف؛ هذا المسار لم يتغيّر في هذا الإصدار.

[1.238.3] — 2026-09-27

تبقى الصفحات مستجيبة أثناء الفحص، ولم تعد سجلات ضمان الجودة عن بُعد تكشف مضيف الإنتاج.

تم الإصلاح

  • كانت لوحة المعلومات والتقارير تستغرق ثوانيَ للتحميل وتنتهي مهلتها أثناء الفحص. كان /api/reports و/api/dashboard يقرآن جميع التقارير ويحللانها مع كل طلب — نحو 3.7 ثانية على خادم الإنتاج، مع حجب كل الطلبات الأخرى؛ وأثناء الفحص كانت تحميلات الصفحات تبلغ مهلة الـ 30 ثانية. تُخزَّن الآن ترويسات التقارير المحللة مؤقتًا لكل ملف وتُحدَّث عند تغيّره، فلا تكلّف القائمة سوى فحص واحد للملف لكل تقرير (500 تقرير: 430 ms ← 3 ms محليًا).
  • كان يمكن لنتيجة من ضمان الجودة عن بُعد أن تطبع جزءًا من اسم مضيف الإنتاج في سجل عام. لا يخفي GitHub السر إلا حين يظهر كاملًا، وكانت رسالة خطأ مقطوعة عند 180 حرفًا تنتهي في منتصف اسم المضيف. تُحجب الآن النتائج والتقارير كاملة قبل قطعها.
  • كان كل تقييم بالذكاء الاصطناعي يُعلَّم بأنه غير سليم البنية. لم يطلب الموجّه قط كتلة ---SCORE_SUMMARY--- التي يتطلبها فحص التقرير (تطلبها سكربتات التقييم في المشروع الأب)، وكان التقرير المترجم يترجم كلمة “Block” أيضًا، فلا يجد الفحص أقسامه. يطلب الموجّه الآن الملخص وحرف الكتلة في كل عنوان، ويقبل الفحص كلمة مترجمة قبل الحرف، وتُزال كتلة الملخص قبل عرض التقرير أو حفظه.

أُضيف

  • الاختبارات 3488 ← 3500.

ملاحظات

  • لا يزال الماسح يعمل داخل عملية الخادم؛ يزيل هذا الإصدار عائق الحجب الكبير الآخر لكنه لا ينقل الفحص إلى worker.

[1.238.2] — 2026-09-27

إصلاح أخطاء 502 التي كان خادم الإنتاج لا يزال يعرضها أثناء الفحص كل ساعة.

تم الإصلاح

  • كانت الصفحات والملفات تعيد 502 أثناء الفحص. يغلق Node اتصال keep-alive الخامل بعد 5 ثوانٍ، بينما يعيد Caddy استخدام الاتصالات الخاملة حتى دقيقتين. وبينما كان فحص داخل العملية يشغل الخادم، كان Node يغلق مقابس أرسل عليها Caddy طلبًا للتو، فيرد Caddy بـ 502 connection reset by peer (‏62 مرة خلال فحص واحد، والخادم نفسه سليم). يُبقي الخادم الآن الاتصالات الخاملة 125 ثانية — أطول من الوكيل — فيكون الوكيل هو من يغلق أولًا دائمًا.

أُضيف

  • يعرض deploy.yml ← verify سبب رد الوكيل بـ 502 (أنواع الأخطاء، مع إخفاء العناوين)، وعدد أخطاء 502 في الدقيقة منذ آخر إعادة تشغيل، ونتيجة الفحص كل ساعة.
  • يستطيع release.yml إنشاء وسم مفقود عند تشغيله يدويًا، ليمكن إصدار نسخة حيث لا يمكن دفع الوسوم.
  • الاختبارات 3486 ← 3488.

ملاحظات

  • لا يزال الفحص يعمل داخل عملية الخادم وقد يشغلها لثوانٍ؛ ونقله إلى worker ليس جزءًا من هذا الإصلاح.

[1.238.1] — 2026-09-27

إصلاح للاستقرار بعد أول اختبار انحدار حيّ لخادم الإنتاج: فحص واحد في كل مرة وإصلاحات للموقع.

تم الإصلاح

  • كان تشغيل فحصين معًا يُسقط الخادم. كان كل طلب GET /api/stream/scan يشغّل الماسح الكامل داخل عملية الخادم دون أي حماية؛ فتزامن فحص بدأ من الواجهة مع المؤقت الساعي وتوقفت العملية بخطأ JavaScript heap out of memory (مرتين، مع أخطاء 502 في الأثناء). الآن يعمل فحص واحد فقط في كل مرة: يتلقى الطلب الثاني error عبر SSE بالرمز SCAN_BUSY ولا يبدأ شيء. كما يرفع مسار النشر حدّ ذاكرة الخادم (heap_mb، افتراضيًا 448 ميغابايت).
  • كانت صفحتا /help و/changelog في cvstart.org تنزلقان جانبيًا على الهواتف (و/changelog باليابانية والصينية على الحاسوب أيضًا): أصبحت الشيفرة الطويلة وجدول المحتويات تلتف الآن.
  • كانت الهندية غائبة عن بدائل اللغة في خريطة الموقع؛ أصبح فحص i18n للموقع يقارن خريطة الموقع بسجل اللغات.
  • كان القسم 19 من المساعدة يطلب إضافة المفتاح إلى «جميع الـ 8» ملفات لغة — عددها 17.

أُضيف

  • اختبار انحدار عن بُعد (.github/workflows/remote-qa.yml): كل مسار × كل لغة في التطبيق المنشور (قراءة فقط)، وكل صفحة × لغة × هاتف/حاسوب في cvstart.org، وكل رابط في ملفات README والويكي، وعند الطلب فحص حقيقي واستدعاءات ذكاء اصطناعي حيّة بكل لغة.
  • مسار نشر الخادم (.github/workflows/deploy.yml): recon وdeploy (مع تراجع مطابق بايتًا ببايت) وverify (اختبار دخان وتقرير استقرار).
  • الاختبارات 3481 ← 3486.

ملاحظات

  • لم يُخفَّض استهلاك الماسح نفسه للذاكرة في هذا الإصدار: تمنع الحماية تشغيل فحصين معًا ويرفع ملف drop-in السقف. يتطلب تحليل فحص حقيقي لـ 98 مصدرًا شبكة الإنتاج.
  • لم يتغير المؤقت الساعي career-ops-scan؛ أصبح recon يعرض الأوامر التي يشغّلها.

[1.238.0] — 2026-09-27

تكافؤٌ مع المشروع الأصل — career-ops على الفرع main عند 993085ce (الإصدار 1.34.0، و182 تغييرًا منذ de2224a9). أربعة مصادر جديدة، وعشرة إصلاحات موفِّرين معكوسة، وإغلاق ثغرة SSRF على مستوى الماسح بأكمله، وتلميح مجانيّ لمدى ملاءمة العنوان في نتائج الفحص.

أُضيف

  • أربعة مصادر جديدة — 94 ← 98 (93 إنجليزيّة + 5 روسيّة)، و89 ← 93 محوِّلًا إنجليزيًّا.
    • Eploy (provider: eploy) — يقرأ خريطة الموقع /live-jobs.xml الخاصّة بالمستأجر على أيّ مضيفٍ ذي علامةٍ تجاريّة (بروتوكول HTTPS فقط؛ وتُرفض عناوين IP الحرفيّة، وlocalhost، و*.local، و*.internal، والمضيفات ذات المقطع الواحد). واستجابةٌ ليست خريطة موقعٍ (صفحة تسجيل دخول) تُلقي خطأً بدلاً من أن تمرَّ كصفرِ وظائف. وصفحات التفصيل اختياريّة (eploy: { fetchDetails, detailLimit }).
    • HiringRoom (*.hiringroom.com) — يقرأ JSON-LD عبر /portal/jobs أوّلاً، وبطاقات /jobs القديمة احتياطًا؛ وتُعاد بناء عناوين URL الخاصّة بالإعلانات ذات http:// في الحمولة على HTTPS.
    • PeopleSoft Candidate Gateway — يُكتشَف عبر المسار /psc/{site}/EMPLOYEE/HRMS/c/HRS_HRAM_FL.HRS_CG_SEARCH_FL.GBL على أيّ مضيفٍ ذي علامةٍ تجاريّة. ويتبع بنفسه إعادات توجيه الجلسة ذات المصدر نفسه الخاصّة بـPeopleSoft (بحدّ أقصى 10 قفزات، وكلّ قفزةٍ تُثبَّت مجدّدًا على أصل المستأجر وتُفحص مقابل العناوين الخاصّة)، ويُعيد تشغيل نموذج “تحميل المزيد”، ويُعلِّم الجولة الجزئيّة (peoplesoftIncomplete) بدلاً من الإبلاغ عنها كمكتملة.
    • PrevueAPS (*.prevueaps.ca) — يحلّ معرِّف الموقع من صفحة الوظائف، ثمّ يقرأ واجهة JSON البرمجيّة.
  • تلميح ملاءمة العنوان في صفوف #/scan. تحمل كلّ نتيجةٍ شارةً تقريبيّةً قويّة / ذات صلة / ضعيفة تقارن عنوان الإعلان بالأدوار المستهدَفة في config/profile.yml (target_roles.primary + أسماء الأنماط). وهو تقديرٌ مجانيٌّ بالكلمات المفتاحيّة: لا يُرشِّح ولا يُعيد الترتيب ولا يغيِّر الأعداد أبدًا، وتُبيِّن التلميحة ذلك. يُحسَب عند تقديم النتائج، فيظهر تعديل الملفّ الشخصيّ في التحديث التالي. تُرجم ×17. ويعكس التغيير رقم 3261 في المشروع الأصل.

أُصلح

  • الأمان — حارس DNS-rebinding لم يكن يعمل قطّ في فحصٍ حقيقيّ. كان http-json.mjs يتحقّق من العناوين المحلولة فقط عند تسليمه دالة fetch العامّة العارية، لكنّ كلا الماسحين يحقنان makeTimeoutFetch()، فكان كلّ فحصٍ يتخطّى ذلك. فكان يُجلَب مصدرٌ يشير إلى اسم مضيفٍ يُحلّ إلى 10.x أو 127.0.0.1. والآن يعمل الحارس داخل makeTimeoutFetch نفسها، ويُسابَق مع مهلة الفحص كي لا يُعلِّق مُحلِّلٌ متوقِّفٌ فحصًا كاملاً. وُجد في مراجعة هذا الإصدار.
  • الأمان — عناوين IPv6 التي تُضمِّن عنوان IPv4 لم تُعامَل كخاصّة. كان العنوان http://[::ffff:127.0.0.1]/ (الذي يُعيد مُحلِّل URL كتابته إلى [::ffff:7f00:1]) يجتاز isValidJobUrl. والآن تُصنَّف كلتا الصيغتين المنقوطة والسداسيّة عشريّة لِـ::ffff:a.b.c.d / ::a.b.c.d بحسب العنوان المُضمَّن. من ملفّ المشروع الأصل _ip-guard.mjs.
  • انتقلت Rippling إلى واجهة board الإصدار الثاني (ats.rippling.com/api/v2/board/<slug>/jobs)، مع ترقيم صفحات (10 صفحات افتراضيًّا، و50 كحدٍّ أقصى)، وقفل مضيفٍ على روابط الوظائف، والتحقّق من الـslug. وأصبح رابط API مُلصَقٌ يُنتج الـslug الحقيقيّ بدلاً من api.
  • Recruitee — تسرد عروض تعدُّد المواقع الآن كلّ مكانٍ (A · B)، وتُسقَط إعلانات (Sample) التجريبيّة والصفوف عديمة العنوان، ويُحتفَظ بالعرض إذا كان أحد عنوانَي URL الخاصّين به صالحًا.
  • Pinpoint — تُسقَط الإعلانات التجريبيّة المزروعة (فيديو العرض التجريبيّ المكرَّر).
  • قنوات Telegram — لم يعد أوّل سطرٍ يحتوي عدّة رموز | يُقرأ كـTitle | Company، وتُتعرَّف كلماتُ أدوارٍ أكثر. ويبقى تعبير الفرع السيريليّ الحسّاس LOCATIONISH_RE بلا مساس (ADR-0002).
  • Workday — يُستقصى ردّ 422/401/403 في الصفحة الأولى مرّةً واحدة؛ وتُعلِّم صفحة صيانة Workday أو إعادة توجيه الانقطاع اللوحة بأنّها ميّتة (وتُحجَر في الوضع الصارم). وترفض طلبات CXS والتفصيل الآن إعادة التوجيه.
  • BambooHR — تُصنَّف /careers/list المُعاد توجيهها عبر تغذية التضمين العامّة: فارغةٌ تعني لوحةً حيّةً بلا شواغر، وأيّ شيءٍ آخر يعني لوحةً ميّتة.
  • Heckler & Koch / Rheinmetall — يُعاد كتابة heckler-koch.com / rheinmetall.com العاريَين بـwww. كي يظلّ النقل الرافض لإعادة التوجيه قادرًا على الوصول. وترفض Ashby إعادة التوجيه. وتُنظِّم beesite وتيرة الصفحات بدالة delay() القابلة للإلغاء.
  • تطوي مرشِّحات العناوين علامات التشكيل على الجانبين (ماسحا الإنجليزيّة والروسيّة)، فيُلتقَط Développeur بسلبيّةٍ developpeur.
  • تفهم مطابقة الشركات في فترة التهدئة الأسماء غير اللاتينيّة وأشكال الشركات (علامات CJK/الكوريّة، وحرف الـİ التركيّ المنقوط). وكانت أسماء CJK تنهار إلى مفتاحٍ فارغٍ ولا تُطابِق أبدًا.
  • location_filter.strict: true يفشل مغلقًا عند موقعٍ فارغٍ متى ما ضُبطت قائمة سماحٍ أو حظر.
  • Liveness — فحوصاتٌ جديدةٌ عبر الواجهة العامّة لتضمينات Greenhouse (gh_jid)، وArbeitsagentur، وWe Work Remotely؛ وتقرِّر SmartRecruiters الآن بعلم active الخاصّ بالإعلان.
  • PeopleSoft — يُحلّ إجراء نموذج “تحميل المزيد” النسبيّ بجانب صفحة البحث، لا عند جذر الأصل.

ملاحظات

  • الاختبارات 3210 ← 3481. حِزَمٌ جديدةٌ: sources-eploy، وsources-hiringroom، وsources-peoplesoft، وsources-prevueaps، وtitle-fit، وfetch-timeout-dns-guard، وcooldown-company-match-unicode، وliveness-api-more-rungs، وru-scanner-title-accent-fold.
  • لم يُنقَل، مع الأسباب: حدود كلمات Unicode في مرشِّح الموقع (مرشِّح web-ui قائمٌ على السلسلة الجزئيّة، فلا حدّ لإصلاحه — نقلٌ كاملٌ متعمَّدٌ لمرشِّح موقع المشروع الأصل عملٌ مستقبليّ)؛ وإزالة تكرار الطلبات المتماثلة العنوان (تُزيل web-ui التكرار بعنوان URL فقط)؛ وتحذيرات إعادة النشر المجمِّعة، ونطاق نطاقات القائمة السوداء، وإيصالات الصفر غير المُتحقَّق منها، وترحيل scan-runs.tsv (لا تملك web-ui أيًّا من هذه الواجهات)؛ وتقسيم أوجه Workday وعلامات الاقتطاع (تجلب web-ui صفحةً واحدةً لكلّ لوحة)؛ وenrichDate الخاصّ بـBambooHR (لا خطّاف إثراء تاريخٍ)؛ وإعادة هيكلة _http.mjs الخاصّة بالمشروع الأصل للسكون عبر نحو 20 موفِّرًا (تستخدم web-ui أصلاً دالة delay مشتركةً قابلةً للإلغاء)؛ والمُحدِّث، وdoctor، وPDF، وtracker/merge-tracker، وvalidate-profile، وdiscover-new-companies، وتطبيق web/ القائم على Next.js، وبوتات GitHub — خاصّةٌ بسطر الأوامر أو مُرحَّلة.
  • أُبقي متعمَّدًا مختلفًا عن المشروع الأصل: تبقى Heckler & Koch / Rheinmetall على HTTPS فقط (بلا ترقية http←https)؛ وتُعيد لوحة Workday الميّتة في فحصٍ عاديٍّ [] بدلاً من إفشال الفحص.

[1.237.1] — 2026-09-22

تصحيحٌ بعد جولة فحص جودة خارجيّة على v1.237.0. تأكَّد إصلاح Jobstreet من الخارج لأوّل مرّة: فعلى مضيفات AU وNZ وHK وMY، تُعيد الصيغة القديمة /id/job/<id> رمز 404 نظيفًا، بينما تصل الصيغة الجديدة /job/<id> إلى مسارٍ حقيقيٍّ محميٍّ بـCloudflare؛ وعلى id.jobstreet.com يصحّ العكس. وكانت الروابط المفقودة حقيقيّةً. ووُجدت علّتان، كلتاهما فيما أضافه v1.237.0 نفسه.

أُصلح

  • كانت مِسمَّاة رابط الكتاب مُترجَمةً، لكنّ وِجهتها لم تكن كذلك. كانت جميع لوحات SPA الـ17 وجميع لوحات cvstart.org الـ17 توجِّه رابط التذييل إلى Agentic Coding Design Patterns نحو الطبعة الإنجليزيّة، بينما المِسمَّاة الوحيدة المترجَمة — الروسيّة، «Паттерны агентного кодинга» — كانت تعِد بكتابٍ روسيّ. وصار مُطبِّقٌ جديدٌ data-i18n-href (بالشكل نفسه لـdata-i18n-title، ومقصورٌ على القيم المطلقة https://) ومفتاحٌ footer.patternsUrl يوجِّهان ru إلى الطبعة الروسيّة وكلّ لغةٍ أخرى إلى الإنجليزيّة. ويُثبِت اختبار الانحدار هذا الثبات في الاتّجاهين: فالعنوان المترجَم يستلزم رابطًا مُوطَّنًا.
  • تُهاجر SEEK مضيفاتها، والنقل يرفض إعادة التوجيه. فـwww.seek.com.au → au.seek.com وwww.seek.co.nz → nz.seek.com: صفحات التفصيل تُعيد بالفعل 301، وكلا زوجَي المضيفات يخدم اليوم واجهة البحث v5 برمز 200. ولأنّ http-json.mjs يضبط redirect:'error' (وهو دفاعٌ ضدّ SSRF يستحقّ الإبقاء عليه)، فإنّ اليوم الذي يبدأ فيه مسار الواجهة بإعادة توجيه إدخال AU/NZ على المضيف القديم سيفشل — وبما أنّ قائمة السماح تضمّ مضيفات www. فقط، فلن يستطيع أحدٌ الهجرة. وصار الآن au.seek.com وnz.seek.com ضمن ALLOWED_JOBSTREET_HOSTS؛ وتبقى المضيفات القديمة، فتستمرّ الإعدادات الحاليّة والروابط المبنيّة سابقًا بالعمل. وتبقى الشبيهات (au.seek.com.evil.com، au-seek.com) مرفوضةً.
  • الويكي: كان صفّ Agentic Practices في خريطة توثيق الصفحة الرئيسية هو الوحيد من بين اثني عشر صفًّا الذي لم يُفلِت الرمز | داخل [[…|…]]، فقطعه GitHub — رابطٌ ميّتٌ، ووصفٌ مخفيّ. حرفٌ واحد.

ملاحظات

  • تبقى الأعداد كما هي عند 94 مصدرًا (89 EN + 5 RU)، و89 محوِّلًا. والاختبارات 3201 → 3210 (+5 لتوطين رابط التذييل، +4 لمضيفات SEEK).
  • غير قابلٍ للإغلاق من الخارج، مُدوَّنٌ لا مُدَّعًى: Oracle Cloud (حجبت Akamai كلا فحصَي REST)، وتثبيت Personio وappendWorkType (يحتاجان إلى قيدٍ في portals.yml، ولا تملك الواجهة حذفًا لأحدهما)، وحالات نصوص liveness (لا صفحة يمكن التحكّم بها؛ أعطى 40 مسبارًا حيًّا 12 حيًّا، و28 غير حاسمٍ، و0 منتهي الصلاحيّة زورًا).
  • تُدرِج المساعدة ×17 الآن كلا شكلَي المضيف لـAU-Main وNZ-Main. وبنية العناوين بلا تغييرٍ عند 32 H2 / 122 H3.

[1.237.0] — 2026-09-21

تكافؤ مع المشروع الأب — career-ops عند الفرع main عند 93c4302b (VERSION يقرأ 1.33.0، بعد 75 التزامًا مسحوبًا من career-ops-hq/main). لا مصادر جديدة هذه المرّة: أربعة إصلاحاتٍ مُنسوخة، واثنان منها كانا يُضيّعان الإعلانات هنا بصمتٍ.

أُصلح

Jobstreet / SEEK — كان كلّ إعلانٍ أستراليٍّ ونيوزيلنديٍّ وسنغافوريٍّ وماليزيٍّ وهونغ كونغيٍّ يشير إلى صفحةٍ ميتة. كانت parseJobstreetItem تبني عنوان التفصيل بصيغة ${baseUrl}/id/job/${id} بلا شرط. وجزء /id/ ليس مسارًا — بل هو بادئة اللغة الإندونيسيّة، ولا وجود له إلّا على المضيفات الإندونيسيّة. وكلّ مضيفٍ آخر من منصّة SEEK يُجيب بـ404 على /id/job/<id>. وكان web-ui قد أضاف أصلًا www.seek.com.au وwww.seek.co.nz وsg.jobstreet.com وmy.jobstreet.com وhk.jobsdb.com إلى ALLOWED_JOBSTREET_HOSTS، فكان المسح يُعيد تلك الإعلانات مطمئنًّا — وكلّ واحدٍ منها يشير إلى 404. وصار المسار الآن مفتاحه المضيف: فـID_LOCALE_HOSTS (id.jobstreet.com، www.jobstreet.co.id، jobstreet.co.id) يُبقي /id/job/، وكلّ ما عداه يأخذ /job/، وأيّ أصلٍ يتعذَّر تحليله يسقط إلى المسار المشترك بدل أن يرمي استثناءً. وكانت حزمتا اختبارٍ من اختباراتنا نفسها قد جسَّدتا العلّة: إذ أكَّدتا hk.jobsdb.com/id/job/… وwww.jobstreet.com/id/job/…، فصُحِّحتا. Oracle Cloud — صفحةٌ قصيرةٌ في منتصف القائمة كانت تُنهي الجولة وتُسقط ذيل اللوحة. كان شرط التوقّف هو listLen === 0 || listLen < PAGE_SIZE، لكنّ ORC يُقدِّم صفوفًا أقلَّ من الحدّ في منتصف القائمة لأنّ بعض الصفوف تُرشَّح من جهة الخادم. وقياسًا أعلى المجرى لدى American Express: بلغت TotalJobsCount 454، وجاءت الصفحات 200 / 199 / 54 — فقُرئت الـ199 بوصفها النهاية، ولم تُجلَب أبدًا 54 وظيفةً أخيرةً، أي 12% من اللوحة. وصارت الآن الصفحة الفارغة وحدها تُنهي الجولة؛ وحين يُبلغ المستأجر عن إجماليٍّ، يستمرّ الترقيم حتى offset + PAGE_SIZE >= total؛ ولا ينتهي عند صفحةٍ قصيرةٍ إلّا مستأجرٌ لا إجماليَّ لديه يُقاس عليه. وهذا يوافق العُرف الأوسع بأنّ واجهة برمجة التطبيقات «قد تُعيد نتائجَ أقلَّ من العدد المطلوب… حتى لو لم تكن في نهاية المجموعة» (Google AIP-158). وتبقى hasMore مُتجاهَلةً عمدًا — إذ يُعيد بعض المستأجرين hasMore:false في كلّ صفحةٍ من لوحةٍ تضمّ 7000 وظيفة. وبعد أن لم تعد الصفحات القصيرة تُنهي الجولة، صارت MAX_PAGES هي الحاجز الوحيد المتبقّي أمام مستأجرٍ يُبلِّغ بإجماليٍّ زائف، فحظيت باختبارها الخاصّ. Liveness — لم تكن عبارة «This role is closed» تُعرَف، وانتهاء الصلاحيّة الزائف دائمٌ. كان النمط /this job (listing )?is closed/i، وهو يفوِّت صيغتَي role وposition الشائعتَين بالقدر نفسه؛ وعلى لوحةٍ واحدةٍ أعلى المجرى، قِيست 111 من 111 إعلانًا غير مؤكَّدٍ تستخدم «role». فوُسِّع النمط إلى /this (?:job|role|position)(?: listing)? is closed\b(?!-)/i. والذيل \b(?!-) حارسٌ ضدّ الصفة المركَّبة: فـ\b وحده يرفض closedown لكنّه لا يزال يطابق نثرًا حقيقيًّا مثل “This role is closed-loop control of the platform”، والاستباق السلبيّ هو ما يمنع ذلك. Liveness — صار job expired الآن في طبقةٍ رخوةٍ لا يمكنها أن تتغلَّب على زرّ تقديمٍ ظاهر. تُفحَص HARD_EXPIRED_PATTERNS قبل اختبار عنصر التقديم، فكلّ ما وُضع فيها يفوز على إعلانٍ حيٍّ. وكان /\bjob expired\b/i يُطلَق زورًا في أربعة أشكالٍ حقيقيّةٍ من صفحاتٍ حيّةٍ — عنصرٌ دائريٌّ لـ«وظائف مشابهة»، وشريحة مرشِّحٍ «إخفاء الوظيفة المنتهية»، وأسئلةٌ شائعةٌ في التذييل، ونثر closed-loop أعلاه — لأنّ المصنِّف يتلقَّى نصّ الصفحة كلَّه. وصار الآن يقيم في SOFT_EXPIRED_PATTERNS، يُفحَص بعد عنصر التقديم وقبل تخمين صفحة القائمة، ويُعيد expired_body_soft؛ وتبقى لافتة «JOB EXPIRED» المجرَّدة بلا عنصر تقديمٍ منتهيةً كما هي. ولهذا الاتّجاه أهمّيّته: لأنّ انتهاءً زائفًا يُكتَب في تاريخ المسح بوصفه skipped_expired، ثمّ يُرشِّح الوظيفة الحقيقيّة خارج كلِّ مسحٍ لاحقٍ، إلى ما لا نهاية، ما لم يُضبَط scan_history.recheck_after_days.

أُضيف

Personio — صار personio: <slug> صريحًا يُثبِّت المستأجر. تُضمِّن شركاتٌ كثيرةٌ لوحة Personio الخاصّة بها كـiframe في صفحة توظيفٍ تحمل علامتها، فيشير careers_url إلى نطاق الشركة نفسها بينما تعيش التغذية في <slug>.jobs.personio.de؛ ودون تثبيتٍ تنحلّ تلك اللوحات إلى لا شيء. والمعرِّف الفرعيّ (slug) مقيَّدٌ بمجموعة أحرفٍ عبر قائمة سماحٍ مُرسَاة (/^[a-z0-9][a-z0-9-]{0,62}$/i)، والعنوان الناتج لا يزال يمرّ عبر تحليل new URL() القائم، وفحص HTTPS، وبوّابة المضيف PERSONIO_HOST_RE — فالتثبيت ليس أبدًا طريقًا إلى مضيفٍ آخر. ومعرِّفٌ فرعيٌّ يحمل . أو / أو @ أو : أو .. أو فراغًا أو - بادئًا، أو يتجاوز حدّ تسمية DNS البالغ 63 حرفًا، يسقط إلى سلوك api/careers_url المعتاد. وخلافًا للمشروع الأب، تعبير المضيف النمطيّ في web-ui حسّاسٌ لحالة الأحرف، فيُطبَّع التثبيت عبر new URL() — فـAcmeGroup يُحلّ إلى acmegroup.jobs.personio.de بدل أن يبني مضيفًا كانت قائمة سماحه نفسها سترفضه. Jobstreet — خيار appendWorkType الاختياريّ يُلحق بالعنوان أنواع عمل الإعلان، مثل Strategy Consultant [Part time]، كي تتمكَّن مرشِّحات العناوين والفرز من رؤية نوع التوظيف وقت المسح. وهو مُعطَّلٌ افتراضيًّا؛ وworkTypes المفقودة أو الفارغة لا تُحدِث أثرًا.

ملاحظات

تبقى أعداد المصادر والمحوِّلات كما هي: 94 مصدرًا (89 EN + 5 RU)، و89 محوِّلًا EN. ولا يُحرِّك هذا الإصدار سوى عدد الاختبارات، 3164 → 3201 (+13 لـJobstreet، +7 لـOracle Cloud، +9 لـLiveness، +8 لـPersonio). وحذف المشروع الأب providers/telegram.mjs، موحِّدًا على telegram-channel. ويُسجِّل web-ui كليهما بوصفهما مصدرَين ظاهرَين للمستخدم، فلم يُتَّبَع الحذف: إذ كان سيُنزل السجلَّ من 94 إلى 93 ويكسر بصمتٍ كلَّ من ضبط telegram. ولا يُعاد النظر فيه إلّا كإزالةٍ متعمَّدةٍ ومُعلَنة. واكتسبت jd-skill-gap.mjs صيغة عنوانٍ غير مُختصرةٍ لـ«ما نبحث عنه» وتجريدًا لعلامات التخشين. ويُمرِّر web-ui ذلك النصَّ البرمجيَّ تمريرًا لا مرآةً له، فيصل الإصلاح مع نشر المشروع الأب دون أن يستدعي تغييرًا هنا. غير مَنقولٍ، مع السبب: lib/latex-escape.mjs وbrowser-extract.mjs وopenai-eval.mjs وupdate-system.mjs والسقّالة (لا يُرائيها web-ui ولا يُصدِف إليها)؛ ولوحة تحكّم Go الطرفيّة واستمارة إعداد web/ (سطحان منفصلان)؛ وdoctor.mjs وanalyze-patterns.mjs (تمريرتان للقراءة فقط يمتصّ فشلُهما الرخو التغيير). وتباعُد الفرع السيريليّ \p{L} في providers/telegram-channel.mjs نجا من الدمج مع المنبع سليمًا، وتحقَّقنا منه بايتًا ببايتٍ مقابل نسخةٍ سابقةٍ للدمج.

[1.236.0] — 2026-09-18

تكافؤ مع المشروع الأب — career-ops عند الفرع main عند 6a9c84c (VERSION يقرأ 1.33.0، بعد 128 التزامًا). مصدران جديدان، وثلاثة إصلاحاتٍ مُنسوخة — كان اثنان منها يُضيّعان الإعلانات هنا بصمتٍ.

أُضيف

مصدرا مسحٍ جديدان، 92 → 94 (89 EN + 5 RU). فمصدر Python.org Jobs (pythonorg) يقرأ التغذية العامة للوحة؛ ونسبة الوظيفة إلى صاحب عملٍ إلزاميّةٌ، فالصفّ الذي لا يسمّي عنوانُه صاحب عملٍ يُسقَط بدل أن يُملأ باسم القيد في البوّابة. أمّا Generalist World (generalist-world) فيكشط بطاقات اللوحة ويفشل جهرًا أمام أيّ ترميزٍ لا يستطيع تحليله — فبطاقةٌ تغيَّر شكلها ترمي استثناءً بدل أن تُقرأ لوحةً فارغة، وقراءتها فارغةً هي عينُها نمط الفشل الذي يجعل مصدرًا مكشوطًا غير موثوقٍ به. ويظهر كلاهما في مرشِّح المصدر بـ#/scan، وكلاهما مثبَّتٌ بالمضيف (تعبيرٌ نمطيٌّ مُرسًى، وHTTPS فقط)، ويقود كلًّا منهما من الطرف إلى الطرف حزمة اختباراتٍ بنقلٍ وهميّ (43 و36 حالة).

أُصلح

كانت وظيفةٌ في Workday مفتوحةً في عدّة مدنٍ تُسقَط بمرشِّح الموقع. فنقطة نهاية القائمة في Workday تجيب عن الإعلان مُتعدِّد المواقع بـعددٍ حيث يحمل كلّ إعلانٍ آخر مكانًا: "53 Locations". وهذا ليس موقعًا، وbuildLocationFilter يطابق بسلسلةٍ فرعيّةٍ غير حسّاسةٍ لحالة الأحرف — فوظيفةٌ مفتوحةٌ في أوستن و52 مدينةً أخرى لم تطابق أيّ قيدٍ في allow: [austin] وأُسقِطت، بينما نجحت الوظيفة نفسها حين أُدرِجت منفردةً. وقياسًا أعلى المجرى عبر ثلاثة مستأجرين، كانت النسبة 53 of 291 إعلانًا عناصر نائبة، أي إنّ هذه هي الحالة العاديّة لا الحافّة. والأماكن الحقيقيّة للإعلان لا توجد إلّا في مستند التفصيل لدى CXS، فصار كلّ عنصرٍ نائبٍ يكلِّف الآن طلب GET إضافيًّا واحدًا عليه، بحدٍّ أقصى 200 لكلّ إدخال — وبعد هذا الحدّ يُترَك العنصر النائب ظاهرًا بدل أن يُزيَّف، لأنّ إعلانًا لم يُحلّ يجب ألّا يُقرأ أبدًا بوصفه مُحلولًا. والتفصيل الفاشل أو الخالي من الموقع يُبقي الإعلان تمامًا كما أعادته القائمة: فـ"53 Locations" ظاهرةً صادقةٌ بشأن ما تعذَّر حلُّه، بينما موقعٌ فارغٌ كان سيمرّ صامتًا عبر كلّ مرشِّح. ويحمل طلب GET نفسه أيضًا startDate، فيُؤرَّخ الإعلان المُثرَى بدقّةٍ بدل نثر نقطة نهاية القائمة النسبيّ ("Posted 30+ Days Ago" → undefined)؛ وكلّ ما ليس بصيغة YYYY-MM-DD الصِّرفة يُترَك على حاله بدل أن يُمرَّر عبر Date.parse، الذي يترك سلوكه الاحتياطيّ غير القياسيّ لتقدير التنفيذ. صار مستوًى مُصرَّحٌ به لا يُدمِج طلبَي توظيفٍ في إعادة نشرٍ واحدة. فـ"Insurance Specialist II" تصريحُ صاحب العمل نفسِه بأنّ هذا الطلب ليس شقيق الآخر — نطاق أجرٍ ومجالٍ ورقم طلبٍ مختلفة تحت عنوانٍ أساسيٍّ واحد. ولم يكن المُجزِّئ يراه: فـw.length > 3 يُسقِط كلّ رقمٍ رومانيٍّ حتى VIII وكلّ رقمٍ مفردٍ، فتجزَّأ "Insurance Specialist" و"Insurance Specialist II" بالشكل نفسه، وسجَّلا نسبة Jaccard مثاليّة، وعلَّم كاشف إعادة النشر الثاني بوصفه إدراجًا مكرَّرًا للأوّل. ولم يكن لدى role-matcher.mjs الخاصّ بـweb-ui أيّ معالجةٍ للمستوى البتّة، فحطّ هذا هنا آليّة المشروع الأب كلَّها: تُستخرَج المستويات مع طيّ الرومانيّ والعربيّ إلى رقمٍ واحد (فـ"Nurse II" و"Nurse 2" تصريحٌ واحدٌ — وذلك الزوج بعينه كان يختلف خطأً من قبل)، ويجب أن يتّفق مستويان مُصرَّح بهما، ويُعامَل مستوًى في جانبٍ واحدٍ وحده بوصفه إعادة صياغةٍ رخوةً إلّا إذا حمل كلّ عنوانٍ أيضًا كلمةً يفتقدها الآخر، وذلك خلافان لا زخرفة. كانت لوحة Avature المثبَّتة على مرشِّحٍ تتجوَّل مع ذلك في اللوحة العالميّة كلِّها. فواجهة بحث Avature تولِّد بارامتراتِ أوجهٍ غامضةً (?42386=[812132] مرشِّح بلدٍ)، والمُشغِّل الذي يثبِّت لوحةً مضيَّقةً في portals.yml يتوقَّع أن يتجوَّل المسح في تلك اللوحة. وكانت resolveSearch تعيد بناء العنوان بصيغة ${origin}${path} وتُسقِط سلسلة الاستعلام كليّةً — فكلّ طلبٍ، بما فيه الأوّل، كان يتجوَّل في لوحة المستأجر بأكملها: صفحاتٌ أكثر، وطلباتٌ أكثر، وإعلاناتٌ رشَّحها المُشغِّل عمدًا. وصارت الأوجه ترافق الآن كلّ طلبٍ مُرقَّمِ الصفحات، مع تطبيق jobOffset الخاصّ بنا أخيرًا كي لا تختطف أوجهٌ ضالّةٌ مشتركة الاسم ترقيمَ الصفحات أبدًا.

أمن

devalue 5.8.1 → 5.9.2 (تبعيّةً متعدّيةً عبر astro)، وأُغلق به التنبيه الوحيد المفتوح من Dependabot — حجبُ خدمةٍ عند مُدخَلٍ مشوَّه. تبعيّةٌ خاصّةٌ بالموقع وحده؛ ولم تتغيَّر شيفرة وقت التشغيل.

ملاحظات

غير مَنقولٍ، مع السبب في كلٍّ. أعطى المشروع الأب lever مهلة 30 ثانية وmanfred مهلة 25 ثانية مقابل افتراضه المشترك البالغ 10 ثوانٍ — وهذا لا يُنقَل: فناقل مسح web-ui يسمح أصلًا بـ60 ثانية (DEFAULT_SCAN_TIMEOUT_MS)، فكلا اللوحتين تملكان هنا هامشًا أوسع ممّا يمنحه الإصلاح هناك. وازدواج بادئة careers- لدى iCIMS لا يمكن أن يحدث هنا أيضًا: فـweb-ui يأخذ أصل المستأجر مباشرةً من api/careers_url ولا يبني مضيفًا من اسم مستأجرٍ أبدًا؛ وتجوال المضيف الاحتياطيّ الجديد هناك ميزةٌ تناسب إعداد المشروع الأب الأكثر تراخيًا، لا إصلاح عيبٍ. واكتسبت deutschebahn وmanfred إعادة محاولةٍ عند الإجهاض العابر، وهو ما يجعله سقف الـ60 ثانية في web-ui أقلّ إلحاحًا بكثيرٍ — فدُوِّن ولم يُنقَل، كي يكون لمهلة تجوالٍ طويلٍ مستقبليّةٍ أوّل ما يُجرَّب معروفًا سلفًا. وكلّ ما عدا ذلك في دلتا الالتزامات الـ128 هو CLI أو مُتحقِّقٌ أو لوحة تحكّمٍ أو web/ (واجهة المشروع الأب نفسها): liveness-browser، وcheck-jd-archive، وverify-ats/verify-portals/verify-cv-facts، وupdate-system، وset-status، وaudit-portals، وdiscover-ats، وscaffolder، وtest-all، وبيانات الإضافات. أُعيد التحقّق من تباعُدات الفرع بعد السحب: إصلاح \p{L} السيريليّ في providers/telegram-channel.mjs، وملفّ الفرع الخاصّ providers/telegram.mjs، وhermes في web/src/lib/clis.ts. واندمج المنبع career-ops-hq/main كاملًا (صفر التزاماتٍ ناقصة). 3066 → 3164 اختبارًا؛ وتذكر المساعدة الآن 94 محوّلًا (89 EN + 5 RU) في كلّ الحزم السبع عشرة، وتبقى بنية العناوين ثابتةً عند 32 H2 / 122 H3.

[1.235.0] — 2026-09-16

تكافؤ مع المشروع الأب — career-ops عند الفرع main عند 68e6b94 (VERSION يقرأ 1.33.0، بعد 42 التزامًا). إصلاحان مُنسوخان؛ كان أحدهما يُضيّع وظائف MokaHR هنا بصمتٍ.

أُصلح

كانت الوظيفة الثانية من لوحة MokaHR نفسها تختفي بدل أن تُضاف. فـurl-key.mjs يختزل عنوان الإعلان إلى مفتاح مقارنة، وكان يُسقِط الجزء الذي يلي الرمز # دون شرط — وهذا معقولٌ، لأنّ هذا الجزء عرضيٌّ بحتٌ في معظم الحالات (#apply، #section-2). وMokaHR هو الاستثناء في سِجلّنا الخاصّ: فروابط إعلاناته على الشكل …/social-recruitment/{tenant}/{id}#/job/{n}، حيث تشترك كلّ وظائف المستأجر الواحد في المسار نفسه، ولا يوجد مُعرِّف الإعلان إلّا في ذلك الجزء الذي يلي #. فكانت كلّها تُختزَل إلى مفتاحٍ واحد. قِيسَ الأمر قبل الإصلاح: الصق وظيفتَي MokaHR مختلفتين في خطّ الأنابيب وتنتهي بـسطرٍ واحد — بلا خطأ، بلا تحذير، بلا ما يلفت النظر؛ ومرشِّح النَّضارة الخاصّ بالماسح (en-scanner.mjs) يقرأ المفتاح نفسه، فبمجرَّد أن تدخل أيّ وظيفةٍ من ذلك المستأجر إلى تاريخ الفحص، تُطرَح كلّ وظيفةٍ أخرى منه باعتبارها «مَشاهَدةً من قبل». وصارت normalizeUrl تُرقّي مسار #/job/{id} أو #/jobs/{id} المُتعرَّف عليه إلى مفتاح استعلامٍ غرضُه المقارنة فقط، قبل إسقاط ذلك الجزء — ويحتفظ MokaHR باسمه الراسخ mokahr_job_id كي تبقى المفاتيح المكتوبة سابقًا قابلةً للمقارنة، بينما يحصل أيّ مضيفٍ آخر على مُعرِّفٍ داخليٍّ هو _career_ops_fragment_job_id. والمُعرِّف المُرقَّى يُلحَق لا يُعيَّن: فالرابط الذي يحمل هذه البارامتر أصلًا يحمل مُعرِّف إعلانٍ آخر، والكتابة فوقه كانت ستدمج بالضبط الشيئين اللذين وُجد المفتاح لِيُبقيهما مُتفرِّقين. وكلّ جزءٍ غير مُتعرَّفٍ عليه لا يزال يَنهار إلى شيءٍ واحد، وهذا هو النصف الذي كان صحيحًا أصلًا. صار accept-encoding مثبَّتًا الآن في كلّ طلبٍ يصدر من أيّ مصدر. فإن تُرك غير مُعيَّن، تتفاوض Node على أيّ ترميزٍ يعرضه إصدار undici المُضمَّن فيها؛ ومن Node ≥ 23 يشمل ذلك zstd، واستجابة amazon.jobs المضغوطة بـzstd تعود مبتورةً عند البايت 1024 تمامًا مع حالة 200 — فيُقرأ الفشل على أنّه Unterminated string in JSON at position 1024، وكأنّه API مشوَّه، لا عطبٌ في طبقة النقل. وكلّ طلبٍ يرسل الآن gzip, deflate, br. وعلى بيئات التشغيل لدينا هذا إجراءٌ وقائيّ لا إصلاحيّ: فـNode 18/20/22 — مصفوفة CI والخادم (22.22.1) — ترسل gzip, deflate، وقد تحقَّقنا من ذلك مباشرةً لا افتراضًا، فلم يُبتَر شيءٌ هنا؛ وما يتغيَّر اليوم هو أنّ br صار مطروحًا، وهو ما لا تطلبه هذه الإصدارات افتراضيًّا لكنّها تفكّ ترميزه بشكلٍ صحيح. ويُطبَّق هذا الافتراضي بصرف النظر عن حالة الأحرف، وهذا هو الجزء الجدير بالمراجعة: فأسماء الترويسات لا تُفرِّق بين الأحرف الكبيرة والصغيرة، وتدمج fetch أيّ مُدخَلين بالاسم نفسه في قيمةٍ واحدةٍ مفصولةٍ بفواصل، فلولا ذلك لكان المستدعي الذي يمرِّر Accept-Encoding: identity يُرسِل بدلًا منه gzip, deflate, br, identity.

ملاحظات

تبقى الترويسات كائنًا عاديًّا عند حدود fetchImpl، حيث يُسلِّم المشروع الأب إلى fetch كائن Headers. وهذا تباعُدٌ مقصودٌ لا سهوًا: فمساعدات fetch في web-ui تأخذ fetchImpl قابلةً للحقن، وخمسٌ وأربعون حزمة اختباراتٍ خاصّةً بالمصادر تقرأ opts.headers['User-Agent'] ممّا تلتقطه — وكائن Headers لا يملك وصولًا بالأقواس المربَّعة، فالنقلُ الأمينُ هنا هو نقل السلوك (الكتابة فوق القيمة بصرف النظر عن حالة الأحرف، وفوز المستدعي) لا نقل التنفيذ ذاته. وكائن المستدعي يُنسَخ ولا يُعدَّل أبدًا، لأنّ عدّة مصادر تتشارك ثابت ترويسةٍ واحدًا. أمّا إصلاح المشروع الأب المرافق — تطبيق وكيل مستخدمٍ افتراضيّ بصرف النظر عن حالة الأحرف — فهو غير مَنقول: إذ لا يحقن web-ui أيّ وكيل مستخدمٍ افتراضيّ أصلًا، وكلّ مصدرٍ يضبط الوكيل الذي تشترطه بوّابته، فلا يوجد افتراضٌ يمكن أن يتصادم مع User-Agent الخاصّ بالمستدعي. تُثبِّت حزمتا اختباراتٍ جديدتان وثلاث حالاتٍ جديدةٍ هذين الإصلاحين: tests/http-accept-encoding.test.mjs (القيمة المُثبَّتة بالضبط، وكلّ واحدةٍ من مساعدات fetch الأربع، وتجاوزٌ من المستدعي بثلاث صياغاتٍ لحالة الأحرف — يُثبِت أنّ مفتاح accept-encoding واحدًا هو مَن يبقى، وهو ما كانت علّة الدمج تُنتِج منه اثنين)، وtests/parsers-pipeline-hash-route.test.mjs الذي يفحص المَعبَر الظاهر للمستخدم لا المفتاح نفسه: يجب أن تصل وظيفتا MokaHR كلتاهما إلى خطّ الأنابيب، وإعادة لصق الوظيفة نفسها بأربع صياغاتٍ يجب أن تنهار إلى سطرٍ واحدٍ كما كانت، وجزءٌ زخرفيٌّ يجب ألّا يتحوَّل إلى عنصر تمييزٍ في المفتاح. وتأكَّد فشل الجميع أوّلًا. التثبيت نفسه يقع في طبقة النقل، لا في تلك المساعدات وحدها: 25 من أصل 92 مصدرًا تستدعي fetchImpl المحقون مباشرةً ولا تمرّ بها إطلاقًا، لذا يطبّقه makeTimeoutFetch — الغلاف الذي يحقنه الماسحان فعليًا — وهو نقطة العبور الوحيدة التي تشترك فيها كل المصادر. (المشروع الأب لا يحتاج مكافئًا: providers/_http.mjs هو طبقة نقله الوحيدة.) صار هناك حارس بنيوي يُسقط التكامل المستمر إذا استدعى مصدر جديد fetch مباشرةً بدل استخدام طبقة النقل المسلَّمة إليه، وحالة سلوكية تُشغّل greenhouse — أحد الـ 25 — عبر الغلاف لتثبت أن الترويسة تصل فعلًا. وغير منقولٍ، مع السبب: liveness-browser.mjs (حارس إعادة استخدام Playwright ذي الواجهة — لا يُشغِّل web-ui متصفِّحًا ذا واجهةٍ)، وcheck-jd-archive.mjs (لا يوجد تناوبٌ له في web-ui)، وtest-all.mjs (أداة تشغيل اختبارات المشروع الأب؛ وتباعُد الفرع الخاصّ بـvpFixtureEnv باقٍ كما هو)، وweb/ (واجهة المشروع الأب نفسها، غير منسوخة)، وبيانات الإضافات وملفّ SIGNATURES.md (خاصّان بالمستودع وحده). أمّا batch/batch-runner.sh (حيث يُهرَّب علامتا العطف قبل تبديل sed) وjd-skill-gap.mjs (الذي يحلّ cv.md من جذر البيانات) فهما مُناوَبان للقراءة فقط — يستدعي web-ui صَدَفةً نسخة المشروع الأب منهما، فيصلان كلاهما دون أيّ تغييرٍ في الشيفرة هنا، وملف jd-skill-gap يجعل تناوب /api/jds/:id/skill-gap أصحّ: فـcv.md يُحَلُّ الآن من جذر البيانات بدلًا من أيّ مجلَّدٍ نشأت فيه العملية. أُعيد التحقّق من تباعُدات الفرع بعد السحب: إصلاح \p{L} السيريليّ في providers/telegram-channel.mjs، وملفّ الفرع الخاصّ providers/telegram.mjs، وhermes في web/src/lib/clis.ts. عدد المصادر ثابتٌ عند 92 (87 إنكليزيّة + 5 روسيّة)، فالمساعدة لم تُمَسّ. 3042 → 3066 اختبارًا.

[1.234.0] — 2026-09-15

تكافؤ مع المشروع الأب — career-ops عند الفرع main عند 56cce8f (لا يزال VERSION يقرأ 1.32.0، بعد 58 التزامًا تجاوزت 1.32.0). إصلاحٌ واحد مُنسوخ عبر 15 مصدرًا، مع حارسٍ يمنع عودته.

أُصلح

مُعرِّف وظيفةٍ واحدٌ مشوَّه لم يعد يُفرغ الصفحة كلَّها. فـencodeURIComponent يرمي URIError عند محرف UTF-16 تعويضي منفرد، وحمولة JSON قد تحمل واحدًا — وJSON.parse('"\\uD800"') يُبقيه كما هو. وكانت خمسة عشر مصدرًا تبني عنوان كلّ إعلانٍ من مُعرِّفٍ أو slug يتحكّم فيه المضيف داخل حلقة التحليل، فيُجهِض إعلانٌ سيّءٌ واحدٌ الحلقةَ ويَخسر كلَّ وظيفةٍ في تلك الصفحة؛ ولم يُسجَّل شيءٌ كفشل مصدرٍ لأنّ الاستثناء كان يتصاعد قبل أن يصل إلى ما يُبلِّغ به الالتقاط الخاصّ بكلّ شركة. أصلحه المشروع الأب في providers/_safe-url.mjs (بحيث تُعيد safeEncodeURIComponent القيمة null عند URIError وتُعيد رمي ما عداه)، ومرَّر مُعرِّف كلّ حلقةٍ عبرها، فتُسقِط الإعلانَ الواحد فقط عند null. وذلك المساعد صار الآن server/lib/sources/_safe-url.mjs، ويُطبَّق التغيير نفسه على المصادر الثلاثة عشر المنسوخة — alibaba وarbeitsagentur وbamboohr وfeishu-jobs وgarena وjibeapply وmanfred وmeituan وmokahr وphenom وthehub وtkms وvdab — وعلى مصدرين خاصَّين بـweb-ui فقط كشفهما الحارس الجديد من أوّل تشغيلٍ له، هما jobstreet وtrudvsem، وكان لهما شكل الحلقة نفسه والرَّمي الكامن نفسه. والنطاق مقصودٌ: فالمعرِّفات الآتية من API وحدها تمرّ عبر المساعد. أمّا القيم المُستمدَّة من الإعداد فتُبقي ترميزها الصارم وتفشل بصوتٍ عالٍ — فـoffice في garena (وهو مقطعٌ من portals.yml) يبقى على urlSegment الذي يرمي، بينما يحصل id الآتي من API على idUrlSegment الذي يُعيد null؛ وcorpName الخاص بـcsod، وslug الخاص بـ4dayweek المتحقَّق منه بـSLUG_RE، مُدرَجان في قائمة السماح بالاسم مع ذكر السبب. العنوان الاحتياطيّ في rheinmetall كان يمكن أن يرمي عند href مشوَّه. فحين لا تحمل البطاقة عنوانًا رئيسًا يُعاد بناء العنوان من slug الرابط عبر decodeURIComponent، وهو يرمي عند تتابع نسبةٍ مئويةٍ فاسدٍ في href المُستخرَج بالكشط — الشكل نفسه: صفٌّ سيّءٌ واحدٌ يقتل الصفحة كلَّها، لكن من جهة فكّ الترميز هذه المرّة. وصار الآن يفكّ الترميز بحذرٍ، ويرجع إلى الـslug الخام عند الفشل. سِجلّ المصادر صار يتخطّى الملفات ذات البادئة _. كان الاكتشاف التلقائيّ يستورد كلّ ملفّ .mjs في server/lib/sources/ عدا نفسه، ويُحذِّر من أيّ ملفٍّ بلا export const meta — فكان المساعد يُنتِج تحذيرًا وقت الإقلاع. وقد اعتُمد عرف المشروع الأب: فالشرطة السفلية البادئة تعني ليس مصدرًا؛ فمثل هذه الملفات لا تُستورَد ولا يُحذَّر منها. أمّا الملف غير المُعلَن وبلا meta فلا يزال يُحذَّر منه، وصار اختبار الاكتشاف يتحقّق من الشِّقَّين معًا (وتأكَّد فشله أمام المرشِّح القديم). كما يعدّ سكربت بناء الموقع sync-assets.mjs ملفات المحوّلات على القرص بالقاعدة نفسها، فلم يعد المساعد يُطلق حارسه ضد الإسقاط الصامت.

ملاحظات

وtests/sources-url-encoding-surrogate.test.mjs ينسخ حزمة اختبارات المشروع الأب في ثلاثة أجزاء: عقد المساعد (المحرف التعويضي المُزاوَج — رمزٌ تعبيريٌّ عاديّ — لا يزال يُرمَّز؛ والرَّمي الصادر عن toString الخاصّة بالقيمة نفسها هو عيبٌ في المستدعي فيتصاعد كما هو)، وسلوك كلّ مصدرٍ من الخمسة عشر (مُعرِّفٌ سيّءٌ واحد + مُعرِّفٌ سليم واحد → لا رَمي، يُبقى السليم ويُسقَط السيّء، كلٌّ عبر المحلِّل النقيّ المُصدَّر من المصدر، أو عبر جالِبه مع نقل وهميّ في حالة thehub)، وحارسٌ على مستوى المصدر: فكلّ ملفٍّ مُحوَّل يجب أن يستورد المساعد ويستدعيه، ولا يجوز لملفٍّ أن يشير إليه دون استيراده، ولا يجوز لملفٍّ لم تُراجَع بنيتُه أن يبني عنوان وظيفةٍ بـencodeURIComponent عاريةٍ — فمصدرٌ جديدٌ بالشكل القديم يُفشِل CI مع رقم السطر المخالف. وتأكَّد فشل حالات الخمسة عشر مصدرًا كلِّها قبل النقل. وكلّ ما عدا ذلك في دلتا المشروع الأب غير مَنقول، مع السبب: batch/batch-runner.sh --cli رايةٌ خاصّة بالـCLI وحده (web-ui لا يستدعي batch-runner عبر الصَّدفة)؛ وinvite-match.mjs صار يحلّ set-status.mjs من جذر الشيفرة — وweb-ui لا يُمرِّره؛ وcompany-funded.mjs اكتسب تغيير safe-url نفسه داخليًّا، وشكل مُخرَجه --json — الذي يقرؤه /api/company-funded — دون تغيير؛ وtest-all.mjs نقل ميزانية مهلةٍ فقط (وتباعُد الفرع الخاصّ بـvpFixtureEnv باقٍ كما هو)؛ وweb/ هو واجهة المشروع الأب نفسها وغير منسوخ؛ وAGENTS.md/SIGNATURES.md خاصّان بالمستودع وحده. أُعيد التحقّق من تباعُدات الفرع بعد السحب: إصلاح \p{L} السيريليّ في providers/telegram-channel.mjs، وملفّ الفرع الخاصّ providers/telegram.mjs، وhermes في web/src/lib/clis.ts. عدد المصادر ثابتٌ عند 92 (87 إنكليزيّة + 5 روسيّة)، فالمساعدة لم تُمَسّ. 3022 → 3042 اختبارًا.

[1.233.2] — 2026-09-10

أُصلح — تنبيهان من CodeQL بدرجةٍ عالية (js/remote-property-injection)، أُغلقا بجعل شرطٍ قائمٍ بنيويًّا لا مُدَّعًى.

أُصلح

فـPOST /api/config ينقل القيم المحفوظة إلى process.env كي تسري بلا إعادة تشغيل، وكانت تلك الحلقة تدور على Object.entries(safe) — وهو كائنٌ مبنيٌّ من جسم الطلب — وفي جوفها حارسٌ صريحٌ ضدّ مفاتيح النموذج الأوّليّ. والشيفرة كانت آمنة، إذ يُملأ safe قبل عشرين سطرًا من KNOWN_KEYS وحدها. لكنّ الحلقة وحدها تُقرأ هكذا: أسنِد إلى خاصّيةٍ اسمُها من كائنٍ بُني من جسم الطلب، ولا يستطيع المحلّل تتبّع القيد عبر حلقتين. صارت الآن تدور على المصفوفة الثابتة مباشرةً، فاسم الخاصّية حرفيٌّ على مستوى الوحدة بالبرهان. والسلوك نفسه لم يتبدّل.

ملاحظات

كان الخيار الآخر ردَّهما بوصفهما إنذارَين كاذبَين، وللمستودع عرفٌ في ذلك. غير أنّ الإصلاح أَولى: فالتوكيد في تعليقٍ أقلّ قيمةً من شكلٍ يتحقّق منه المحلّل والقارئ التالي بلا تتبّع. أمّا الاختبار الذي كان يُثبّت الحارس القديم فكان بحثًا في نصّ المصدر. استُبدل باثنين أقوى: بنيويٍّ (تأكّد سقوطُه أمام الشكل القديم)، وسلوكيٍّ يرسل __proto__ بوصفه JSON خامًا — إذ إنّ { __proto__: … } في المصدر يضبط النموذج الأوّليّ ولا يُنشئ خاصّية. 3021 ← 3022 اختبارًا.

[1.233.1] — 2026-09-10

أُصلح — لم يكن الدليل يصف العنصر الذي أضافته v1.233.0، وكان القسم نفسه لا يزال يحمل إرشاداتٍ بطل صدقها منذ مئتَي إصدار.

أُصلح

DOC-1 — صار كلّ قائمةٍ منسدلة يبدأ بـ«استخدام القيمة الافتراضية (…)»، وهو يعبّر عن حالٍ لم يكن للواجهة سبيلٌ إلى إظهاره: أنّ المفتاح غير مضبوط. والفرق بينه وبين اختيار القيمة نفسها من القائمة خفيٌّ وذو أثر. أُضيفت ثلاث نقاط إلى قائمة ### Behavior القائمة في السبعة عشر بندًا كلّها. DOC-2 — كان صفٌّ في الجدول لا يسمّي مفتاحًا البتّة: ففي خانته (server uses default UA) تصف HH_USER_AGENT الذي أزالته v1.19.0، ثمّ تدعو إلى التسجيل في dev.hh.ru — وهو عملٌ لا طائل تحته منذ v1.65.0. أُزيلت الإشارات الثلاث المتقادمة كلّها؛ أمّا صفّ استكشاف الأخطاء فأُعيدت كتابته لا حذفه، إذ نصفه صحيحٌ بعد: فمنذ تمّوز 2026 يردّ hh.ru بـHTTP 451 على العناوين غير الروسيّة. وصار جدول المفاتيح يقول ما هو. فهو يعدّ ستّةً من خمسةٍ وأربعين، وعنوانه يَعِد بسجلّ. جملةٌ واحدة ×17 تُعلن أنّه انتقاء وتُحيل إلى #/config.

ملاحظات

وtests/help-recognized-keys.test.mjs هو البوّابة التي كانت غائبة: يقابل العمود الأوّل من جداول المفاتيح بـKNOWN_KEYS الحيّ. 3018 ← 3021 اختبارًا؛ وتبقى المساعدة 32 H2 / 122 H3.

[1.233.0] — 2026-09-10

أُصلح — ثلاثة مواضع في مسار الحفظ بـ#/config؛ وCONFIG-3 من صنع إصلاح v1.232.1 نفسه.

أُصلح

CONFIG-3 — اختيارٌ مقصودٌ صادف أن يساوي القيمة المبذورة كان يُطرح بوصفه «لم يُمَسّ». فـinitial يخلط أصلين: البذر من .env (والمساواة تعني أنّ الحقل لم يُحرَّر، فتخطّيه صواب)، والبذر من defaultValue (والمساواة ملتبسة: إمّا لم يُفتح، وإمّا فُتح ورُضي به). ومع غياب HOST يعرض الحقل 127.0.0.1؛ فإدخال هذه القيمة عينها ثمّ الحفظ يرسل {} ويبقى المفتاح غائبًا. أُصلح بتسجيل seededFromFile إلى جانب initial. وكان حذف مفتاحٍ يُبلَّغ بـ«· 0» لأنّ الخادم لا يردّ إلّا بما كُتب. صار يردّ بـremoved أيضًا، ويعدّ التنبيهُ الاثنين. وكانت القائمة تُضبط ولا تُفرَّغ: فلا واحدةٌ من الثمانية عشر تعرض خيارًا فارغًا. صارت كلٌّ تبدأ بـ«استخدام القيمة الافتراضية (…)». ومفتاح i18n واحد جديد ×17.

ملاحظات

خطّ الأساس للاختبارات كما هو: 3018؛ وفي المتصفّح 111 ← 116.

[1.232.1] — 2026-09-10

أُصلح — كان الحفظ في #/config يكتب حقولًا لم يمسّها المستخدم.

أُصلح

CONFIG-2. تبذر v1.57.1 في الحقل غير المضبوط قيمته الافتراضية ليعرض ما يستعمله الخادم فعلًا — وهذا صواب وقد بقي — بينما كان save() يسأل dirty للأسرار وحدها ويرسل سائر الحقول دائمًا. فصارت القيمة الموضوعة للعرض تسافر كأنّها اختيار المستخدم: تغييرُ قائمةٍ واحدة يرسل ثمانيًا وعشرين مفتاحًا ويُثبّت ثمانية عشر لم يفتحها أحد. ولا يتبدّل شيء يومَها، فالمكتوب هو الافتراضيّات نفسها؛ إنّما يتبدّل وضع كلّ إعداد: من «غير مضبوط، اتبع افتراضيّ المشروع» إلى «مثبَّت في .env». وهذه القوائم تتحرّك بين الإصدارات، فمتى غيّرنا افتراضيًّا لن يبلغ التغييرُ صامتًا كلَّ من ضغط الحفظ مرّة.

ملاحظات

وموضع النظر هو أساس المقارنة. فالمقارنة بـcfg.values المحمَّلة — وهو الإصلاح البديهيّ، وأوّل ما جُرّب — لا تكفي: إذ يعرض الـselect غير المضبوط قيمته الافتراضية بينما المحفوظ ''، فيختلفان بحقّ، وتُكتب القوائم السبع عشرة كما كانت؛ فانخفض جسم الطلب من سبعةٍ وعشرين مفتاحًا إلى تسعة عشر لا إلى واحد. أمّا الآن فتُقارَن القيمة بما بُذر به كلّ حقل، وهو التعبير الوحيد عن «لم يُمَسّ» الذي يفصل قيمةً وُضعت للعرض عن مستخدمٍ اختار القيمة نفسها قصدًا. وأُصلح معه: عددُ المفاتيح المكتوبة كان داخل الوسيط الاحتياطيّ لـt()، وconfig.saved موجودٌ في المعاجم السبعة عشر جميعًا، فلم يُعرض العدد قطّ. 3018 اختبارًا كما هي، و105 ← 111 في المتصفّح.

[1.232.0] — 2026-09-09

أُصلح — عيبان كشفهما فحصٌ في المتصفّح، وما جاء أيٌّ منهما من v1.231.5.

أُصلح

PROFILE-1 — كان #/profile يُمرّر الصفحة كلَّها أفقيًّا. فقيمة الحقل كانت <div> مغفلًا لا يحمل إلّا أنماط خطٍّ مضمّنة، فبقي overflow-wrap المحسوب normal. ورابط LinkedIn لا يمنح CSS موضع قطعٍ مشروعًا، فصار عرض min-content هو السلسلة بأكملها؛ وعناصر الشبكة تحمل min-width: auto وتأبى الانكماش دونه — فبعجت البطاقةُ مسارها، ثمّ الصفَّ فالعمودَ الرئيس فالمستند (23 بكسل من التمرير الحقيقيّ عند 1280). أُصلح بصنف مشترك .card-value عليه overflow-wrap: anywhere، ومعه .card-row > * { min-width: 0 } سندًا. و**anywhere لا break-word: فلا يُحسب في min-content إلّا anywhere. CONFIG-1 — كان POST /api/config يقبل أيّ قيمةٍ لحقل select. فـ{"LLM_PROVIDER":"not-a-provider"} يردّ 200 ويُكتب في .env؛ ثمّ لا يفشل شيء، إذ يرتدّ المحلِّل صامتًا إلى بديلٍ فيبدو كلّ شيءٍ سليمًا بينما اختيار المستخدم لا أثر له. وLLM_PROVIDERS، الحاملُ لتلك القيم التسع عشرة بعينها، كان مُصدَّرًا من env-config.mjs طوال الوقت**؛ وإنّما لم يكن المدقّق يرجع إليه.

ملاحظات

ولا يُتحقَّق إلّا من LLM_PROVIDER، وهو تضييقٌ مقصودٌ لما ورد في التقرير. فالمجال الحقيقيّ لقوائم النماذج يخصّ المزوّد ويتغيّر بين إصداراتنا، وفرضُ قائمتنا المنتقاة يمنع اختيار نموذجٍ يقدّمه المزوّد فعلًا. والنموذج الخطأ يفشل جهرًا عند الاستدعاء؛ وحده اسم المزوّد يفشل صمتًا. والقيمة المحفوظة سلفًا يُتجاوَز عنها، إذ أثبت فحصٌ في المتصفّح أنّ الصيغة الصارمة غير صالحة. فالنموذج يُعيد إرسال كلّ حقلٍ غير سرّيّ مع كلّ حفظ، فرفضُ قيمةٍ قديمة يُجمّد الصفحة بأسرها. أمّا الآن فتُرفض التغييرات وحدها. وأُغلق معه: OLLAMA_API_KEY كان يُقبل عبر الواجهة ولا حقل له في #/config. صار له واصفٌ الآن (سرّيّ، ومفتاحا i18n ×17)، ويضمن اختبارٌ بقاء مفاتيح الخادم الخمسة والأربعين مساويةً لواصفات الواجهة. 3013 ← 3018 اختبارًا.

[1.231.5] — 2026-09-09

أمن — أُغلقت سبعة تنبيهات، أحدها تنفيذ شيفرة عن بُعد بالغ الخطورة. ولم يتغيّر أيّ ملفٍّ من ملفّات المصدر.

أمن

مدموجٌ من Dependabot: multer 2.2.0 ← 2.3.0 (عالٍ — حجب خدمةٍ بتسريب واصفات الملفّات عند الإجهاض)، وsvgo 4.0.2 ← 4.1.0 في الموقع (عالٍ ومتوسّط — كان removeScripts يُمرّر روابط قابلة للتنفيذ عبر معالجة فضاءات الأسماء والمحتوى). ثمّ خمسةٌ أُخَر كشفها إعادة الفحص التي أطلقها ذانك الدمجان. وكلّ مدًى متأثّرٍ كان أصلًا ضمن ما يسمح به مُحدِّد ^ القائم، فلزم تحديثٌ لا تعديلُ بيان: astro 7.1.0 ← 7.3.2 — بالغ، تنفيذ شيفرة عن بُعد عبر تحسين صور AVIF (أُصلح في 7.2.8)، ومعه تجاوزُ تخويلٍ لغياب فحص حدّ مقطع المسار (7.2.4)؛ و**sharp 0.35.3 ← 0.35.4** — عالٍ، ثغرتا libheif؛ و**js-yaml 4.3.1 ← 4.3.2** في كِلا البيانين — عالٍ.

ملاحظات

«التبعيّات وحدها» ليست هنا مرادفًا لـ«بلا مخاطرة». فـjs-yaml يُحلّل كلّ إعدادٍ يقرؤه التطبيق، وmulter يستقبل كلّ رفع، وقد تجاوز Astro إصدارًا ثانويًّا. المتحقَّق منه: npm run test:ci 3013/3013، ومجموعة المتصفّح 4/4، وإعادة بناءٍ كاملة للموقع أعطت 86 صفحة نفسها و17 مرآةً كاملة وfacts.json صحيحًا. ويُبلّغ npm audit عن صفر ثغرات في البيانين.

[1.231.4] — 2026-09-07

أُصلح — أصلحت v1.231.3 ما بعد النقر على Doctor لا لحظتَه. بلاغٌ ثانٍ، من الجوال ومن سطح المكتب.

أُصلح

كان withSpinner لا يزال يُعيد كتابة الزرّ والطلب في الجوّ. جعلته v1.231.3 يلتقط عُقَد الأبناء ويُعيدها في finally، وقد كانت تعود فعلًا — غير أنّ إشارة الانشغال ظلّت تُنفّذ replaceChildren بالنصّ '⏳ ' + original. فطوال مدّة تشغيل doctor.mjs كان مربّع 36 بكسل يحمل النصّ ⏳ 🩺Doctor ويصير قرصًا عريضًا يكسر السطر؛ وعلى سطح المكتب يظهر ⏳🩺Doctor متلاصقًا. فإعادة الأبناء أصلحت ما بعدُ لا اللحظة. والزرّ الذي له أبناءٌ عناصر لم يعد يُعاد كتابته البتّة: إشارته صنف .is-loading، وهو يستبدل الأيقونة بساعةٍ رمليّة في CSS، فلا يمكن للهندسة أن تتغيّر: 36 بكسل في السكون، و36 في الجوّ، و36 بعدُ.

ملاحظات

الدرس في التوكيد لا في الشيفرة. كانت بوّابة v1.231.3 تنتظر زوال aria-busy ثمّ تقيس — فلم يكن بوسعها بنيويًّا أن ترى إلّا الحالة التي أصلحها finally سلفًا. أمّا الفحص الجديد فيُمسك الوعد مفتوحًا بنفسه ويأخذ عيّنته في منتصف الطيران. 3012 ← 3013 اختبارًا.

[1.231.3] — 2026-09-07

أُصلح — النقر على Doctor كان يُفسد شريط الأعلى في الجوال. بلاغٌ من هاتفٍ يوم صدور v1.231.2 نفسه.

أُصلح

كان UI.withSpinner يسحق أبناء الزرّ الذي يزيّنه هو ذاته. فإشارة الانشغال كانت تمرّ عبر textContent: احفظ button.textContent، واكتب '⏳ ' + original، ثم أعد كتابته. والإسناد إلى textContent يستبدل كلّ الأبناء بعقدة نصّية واحدة — وكانت v1.231.2 قد قسمت للتوّ كلّ إجراء في شريط الأعلى إلى .btn-ico و.btn-label، مع إخفاء التسمية تحت max-width: 900px. فأتلفت أوّل نقرة على Doctor كلا الـ<span> إلى الأبد: لم يبقَ للقواعد الجوّالة ما تُمسك به، فعادت التسمية نصًّا عاريًا، ونما مربّع 36 بكسل قرصًا يقرأ 🩺Doctor يركب فوق مبدّل السمة. ولم يكن يُشفى من تلقاء نفسه — وحده إعادة تحميل الصفحة يُعيد الترميز. أمّا الإصلاح فيلتقط عُقَد الأبناء ويُعيدها بـreplaceChildren؛ والأزرار النصّية الخالصة، وهي غالبية مواضع الاستدعاء الأربعة والعشرين، تسلك تمامًا كما كانت.

  • FIND-3 — كان حقل البحث قد انهار إلى 8 بكسل على هاتفٍ عرضه 320 بكسل. فجعل الشريط في سطرٍ واحد أبقى الـsearchbar العنصرَ المرن الوحيد، وقد قال سجلّ التغييرات الماضي ذلك صراحةً: «له أن ينكمش إلى لا شيء». وقد انكمش إلى لا شيء فعلًا: 8 بكسل من الحقل عند 320، و28 عند 340 — حرفٌ واحد من نائبٍ نصّي طوله واحدٌ وعشرون. يُنقر ويُكتب فيه، لكنّه لم يعد يُقرأ حقلًا. ولا يُصلحه أيّ min-width: فعند 320 بكسل يحمل الشريط 32 بكسل حشوًا، وقائمةً بأربعين، وإجراءاتٍ بمئةٍ واثنين وستّين، فلا يبقى 120 بكسل يُعطى. لذا صار الحقل دون 420 بكسل — وهو العرض الذي يتجاوز عنده المئة من تلقاء نفسه — يختبئ خلف عدسةٍ مكبّرة وينفتح على عرض الشريط كلّه عند اللمس: 182 عند 320، و222 عند 360، و252 عند 390، بلا فيضٍ عند أيّ عرض. ويحتفظ الزرّ باسمٍ ميسَّرٍ واحد، ويُبلّغ حالته بـaria-expanded؛ وتبديل 🔍/✕ من CSS خالص — فلا JS يكتب في هذا الزرّ، وتلك الكتابة بعينها هي ما أعطب Doctor في هذا الإصدار نفسه. وفوق 420 بكسل لا يتغيّر شيء. اكتشفه فحصُ انحدارٍ من المتصفّح على البناء الحيّ.

ملاحظات

هذا هو صنف الخلل نفسه الذي وُثِّق في v1.231.2 بوصفه فخّ applyI18n() — شيفرةٌ تُسنِد textContent إلى عنصرٍ صار له أبناءٌ عناصر — بلغناه هذه المرّة من مُستدعٍ آخر. والدرس يتعمّم: ما إن تُقسّم محتوى عنصرٍ إلى <span> أبناء حتى يصير كلّ كاتبٍ قائمٍ لـtextContent على ذلك العنصر عيبًا كامنًا، وهؤلاء الكُتّاب ليسوا دائمًا في الملفّ الذي تُحرّره. أُحكم القفل مرّتين: node:vm فوق withSpinner الحقيقيّ، ونقرةٌ في متصفّحٍ حقيقيّ عند 320 بكسل. وقد تأكّد سقوط كليهما أمام الشيفرة القديمة. 3009 ← 3012 اختبارًا (حالة المتصفّح اختياريّة وليست ضمن test:ci).


career-ops-ui v1.267.1 · page 3/11 · mirror of the project changelog for cvstart.org