دليل تشغيل المعايرة: قياس صعوبة الأسئلة وإعادة ضبطها
هذا هو الدليل التشغيلي لتشغيل معايرة راش غير المتصلة (offline): كيف تتحقّق من أنّ لديك بيانات
كافية، وتُجري عملية الملاءمة (fit)، وتقرأ النتيجة، وتُعيد ضبط الصعوبات كلّما تراكمت استجابات أكثر.
وهو الرفيق العمليّ لصفحة المفهوم وللشيفرة تحت services/calibration/.
من يُشغّله: مهندس أو مسؤول بيانات لديه صلاحية الوصول لقاعدة البيانات. وهو غير متصل, لا يكون أبداً جزءاً من مسار طلبات التطبيق. تقرأ كلّ عملية تشغيل بِركة الاستجابات، وتُلائم صعوبة كلّ سؤال، وتكتب لقطة (snapshot) جديدة مُرقّمة بإصدار. ولا تُعدّل أبداً إصداراً منشوراً في مكانه.
النموذج في فقرة واحدة
يجلس الطلاب والأسئلة على مسطرة واحدة مشتركة للقدرة/الصعوبة (راش 1PL). يُقدّر التطبيق قدرة الطفل (θ)
حيّاً في TypeScript؛ وهذه المهمّة تقيس صعوبة كلّ سؤال (δ) بشكل غير متصل من الإجابات الحقيقية.
وإلى أن يجمع السؤال إجابات حقيقية كافية يبقى على صعوبة مُقدَّرة (delta_prior، «المسار B /
مبدئي»)؛ وبمجرّد تجاوزه العتبة تُعتمَد صعوبته المقيسة («المسار A»). هذا الدليل هو كيف يتخرّج سؤال
من المسار B إلى المسار A, وكيف تُعاد لاحقاً معايرة صعوبته المقيسة.
متى تُشغّلها
- أوّل معايرة حقيقية, بعد أن يولّد فوج تجريبي استجابات كافية (انظر فحص الجاهزية أدناه). قبل ذلك يكون كلّ سؤال مبدئياً ويعمل التطبيق جيّداً على الصعوبات المُقدَّرة.
- إعادة المعايرة, في نهاية كلّ فوج/نافذة اختبار، أو كلّما هبطت دفعة معتبرة من الاستجابات الجديدة. تُعيد كلّ عملية إعادة تشغيل قياس كلّ سؤال مؤهَّل وتسكّ إصداراً جديداً؛ وتُبقي الجلسات الجارية على الإصدار الذي بدأت به، فلا يتزحزح شيء في منتصف الجلسة (V-6).
- لا تُشغّلها بشكل مستمرّ ولا على الخادم الحيّ. هي خطوة دفعيّة مقصودة.
عتبة الثقة
لا تُعتمَد صعوبة السؤال المقيسة إلا بعد أن يجمع ≥ 300 استجابة حقيقية
(CALIBRATION_TRUST_THRESHOLD في كلٍّ من services/calibration/calibration/config.py وفي
modules/engine/engine/two-track.ts على الخادم, ويجب أن يتطابقا). تُترَك الأسئلة دون 300 على
delta_prior دون مساس. هذه هي بوّابة «المسارين».
الخطوة 0: الإعداد (مرّة واحدة لكلّ جهاز)
cd services/calibration
python3 -m venv .venv && source .venv/bin/activate # .venv is gitignored
pip install -r requirements.txt # pinned girth/numpy/scipy, V-6
export DATABASE_URL="$(grep '^DATABASE_URL=' ../../apps/server/.env | cut -d= -f2- | tr -d '\"')"يجب أن يشير DATABASE_URL إلى قاعدة البيانات التي تحوي الاستجابات الحقيقية (قاعدة التجريب/الإنتاج).
جداول المعايرة (calibration_run_log وcalibration_version_snapshot وitem_calibration_snapshot)
مملوكة لهجرات Prisma على الخادم، فهي موجودة هناك مسبقاً, طبّق
pnpm --filter server prisma migrate deploy أوّلاً إن كنت على قاعدة بيانات جديدة.
الخطوة 1: تحقّق من أنّ شيفرة المعايرة تعمل
قبل الوثوق بتشغيل حقيقي، تأكّد من أنّ girth يستعيد صعوبات معلومة:
python -m pytest tests/ -qيُحاكي هذا إجاباتٍ من أسئلة صعوبتها معلومة، ويُلائمها، ويتحقّق من الاستعادة (الارتباط ≥ 0.95، متوسّط الخطأ المطلق < 0.15) والحتميّة (تشغيلان متطابقان). إن فشل هذا فلا تُجرِ معايرة حقيقية، فالبيئة أو إصدار اعتماديّة ما غير صحيح.
يُشغّل الأمر نفسه أيضًا اختبارًا شاملًا يُنشئ قاعدة بيانات مؤقّتة، ويزرع استجاباتٍ اصطناعية، ويُشغّل حلقة التحميل والملاءمة والكتابة كاملةً. ويتحقّق من بوّابة المسارين (أيّ سؤال دون 300 استجابة يبقى مؤقّتًا) ومن تطابق نتيجة إعادة التشغيل بايتًا ببايت. يحتاج هذا الاختبار إلى Postgres ويتخطّى نفسه تلقائيًّا عند تعذّر الوصول إليها، فيبقى فحص الاستعادة أعلاه قابلًا للتشغيل في كلّ مكان.
الخطوة 2: تحقّق من توفّر البيانات التي تحتاجها
لا تستحقّ المعايرة التشغيل إلا إذا تجاوزت الأسئلة 300 استجابة. شغّل هذا على نفس DATABASE_URL. يَعُدّ
كلّ استجابة (طالب، سؤال) يقرؤها المُحمِّل — صفوف item_response لمسارات التشخيص والتدريب والورق
والتأكيد، ما عدا الإجابات المستبعدة (asset_failed) — ويُبلّغ كم سؤالاً جاهزٌ للمعايرة لكلّ
مهارة فرعية:
WITH responses AS (
SELECT ir."itemId" AS item_id, ir."subSkillId" AS sub_skill_id
FROM item_response ir
WHERE NOT ir.excluded AND ir.source IN ('diagnostic', 'practice', 'paper', 'confirmation')
),
per_item AS (
SELECT item_id, sub_skill_id, count(*) AS n
FROM responses
GROUP BY item_id, sub_skill_id
)
SELECT sub_skill_id,
count(*) FILTER (WHERE n >= 300) AS items_ready, -- will be calibrated (Track A)
count(*) AS items_seen, -- have any responses
max(n) AS most_responses_on_one_item
FROM per_item
GROUP BY sub_skill_id
ORDER BY items_ready DESC;اقرأها هكذا:
items_ready= 0 في كلّ مكان → لا بيانات كافية بعد؛ سيكتب التشغيل الحقيقي سِجلّاً فارغاً ولن يعاير شيئاً. انتظر استجابات أكثر.- تحتاج المهارة الفرعية إلى سؤالين جاهزين على الأقلّ (
MIN_ITEMS_PER_CELL) كي تُلائَم خليّتها، ويُتخطّى السؤال الجاهز المنفرد. - وضعٌ سليم → عدّة مهارات فرعية لكلٍّ منها بضعة
items_ready.
الخطوة 3: تشغيل تجريبي (يُلائم لكن لا يكتب شيئاً)
شاهِد بالضبط ما الذي سيُعايَر والصعوبات التي سيُسنِدها، دون المساس بقاعدة البيانات:
python -m calibration.calibrate --dry-runيطبع لكلّ سؤال مؤهَّل: δ (الصعوبة المُلائَمة، مُمركَزة حول المتوسّط)، وse (خطؤها المعياري)، وn
(الاستجابات المستخدمة)، وinfit (إحصائيّة ملاءمة, انظر الخطوة 5). تحقّق يدوياً من أنّ ترتيب δ
يطابق الحدس (الأسئلة التي تتوقّعها صعبة تخرج أعلى). وإن أبلغ عن 0 عناصر مؤهَّلة فقد أخبرتك الخطوة 2
بالسبب مسبقاً.
الخطوة 4: شغّل المعايرة الحقيقية
python -m calibration.calibrate --triggered-by cohort_endفي معاملة واحدة يقوم هذا بـ:
- إدراج صفّ
calibration_run_log(ترويسة التدقيق)، - سكّ
calibration_version_snapshotالتالي (رقم إصدار جديد متزايد)، - كتابة صفّ
item_calibration_snapshotواحد لكلّ سؤال مُعايَر (مُجمَّد، لا يُحدَّث أبداً)، - تحديث صفّ
item_bankلكلّ سؤال مُعايَر:deltaCalibratedوcalibrationNوcalibrationVersion = <الجديد>وcalibrationProvisional = false.
لا تُمَسّ الأسئلة دون 300, تبقى على delta_prior / المسار B. وأيّ تشغيل بلا عناصر مؤهَّلة يكتب
سِجلّاً نظيفاً status = completed_empty ويتوقّف (ولا يسكّ أبداً إصدار ضوضاء).
الخطوة 5: راجِع النتيجة
افحص التشغيل وجودة ملاءمته:
-- the run header
SELECT id, calibration_version, status, items_calibrated, response_count,
fitting_method, config_version, started_at, completed_at
FROM calibration_run_log ORDER BY id DESC LIMIT 5;
-- per-item difficulties + fit statistics for the version you just minted
SELECT item_id, sub_skill_id, delta_calibrated, delta_se, calibration_n,
infit_mnsq, outfit_mnsq
FROM item_calibration_snapshot
WHERE calibration_version = (SELECT max(calibration_version) FROM calibration_version_snapshot)
ORDER BY sub_skill_id, delta_calibrated;ما الذي تتحقّق منه:
status = completed، وitems_calibratedيطابق عدد جاهزيّتك.infit_mnsq/outfit_mnsqإحصائيّتا ملاءمة للسؤال. القيمة 1.0 مثاليّة؛ والمدى المنتج المعتاد 0.5-1.5. قيمةٌ أعلى بكثير من 1.5 تعني أنّ السؤال يتصرّف بشكل غير منتظم (من يُفترض أن ينجحوا فيه يفشلون والعكس), ضَعْه قيد مراجعة المحتوى، ولا تشحنه بصمت. أمّا دون ~0.5 فيعني أنّه متوقَّع أكثر من اللازم (غالباً شبه مكرّر أو سهل جدّاً).delta_seكبير على سؤال → صعوبته ما تزال مهتزّة؛ تجاوز 300 لكنّ الاستجابات كانت غير متوازنة. راقِبه الجولة القادمة.delta_calibratedمُمركَزة حول المتوسّط لكلّ ملاءمة (صعوبة راش مُعرَّفة فقط حتى ثابت)، فلا تقارن القيم المطلقة بين المهارات الفرعية, قارن الترتيب داخل المهارة الفرعية الواحدة.
الخطوة 6: أكِّد أنّها سَرَت في التطبيق
لا حاجة لنشر أيّ شيء آخر. يقرأ الخادم آخر إصدار منشور تلقائياً
(getCurrentCalibrationVersion() ← MAX(calibration_version))، فتُثبِّت الجلسة التشخيصية/التدريبية
التالية إصدارك الجديد وتقدّم δ المقيسة للأسئلة المُعايَرة (المسار A)؛ ويبقى كلّ ما عداها على
delta_prior (المسار B). يحمل صفّ تدقيق السؤال المُقدَّم calibrationProvisional = false بمجرّد
وجوده على δ مقيسة. وتُبقي الجلسات الجارية على إصدارها المُثبَّت, فلا تقفز في منتصف المسار.
إعادة ضبط الصعوبة لاحقاً (الحلقة المتكرّرة)
عند وصول استجابات أكثر، فقط شغّلها مجدّداً (الخطوة 2 ← الخطوة 4). حقائق أساسية:
- تُعيد إعادة التشغيل قياس كلّ سؤال مؤهَّل من بِركة الاستجابات الكاملة وتكتب إصداراً جديداً (مثل 2، 3، …). ولا تكتب أبداً فوق صفوف لقطة الإصدار 1, تبقى للتدقيق وإعادة التشغيل.
- السؤال الذي على المسار A تُعاد معايرة δ الخاصّة به إلى القياس الجديد؛ والسؤال الذي تجاوز 300 حديثاً يتخرّج من المسار B إلى المسار A لأوّل مرّة.
- يعكس
item_bankدائماً δ آخر إصدار؛ ويعيش تاريخ كلّ إصدار فيitem_calibration_snapshot. - الإيقاع المُوصى به: مرّة لكلّ فوج/نافذة اختبار. تجنّب إعادة التشغيل على زيادات ضئيلة, اترك دفعة معتبرة تتراكم كي يتحرّك إعادة القياس فعلاً.
التراجع / تثبيت إصدار
الإصدارات إلحاقيّة فقط؛ لا تحذف أو تعدّل واحداً أبداً. لإرجاع التطبيق إلى معايرة أسبق، أنت لا
تتراجع عن شيء, يمكن تثبيت الجلسة على calibrationVersion محدّد (لا يثق المساران بِـ δ مقيسة إلا حين
يكون calibrationVersion ≤ الإصدار المُثبَّت للجلسة). وإن شُحنت معايرة سيّئة، فالحركة الآمنة هي سكّ
إصدار جديد مُصحَّح من بيانات منقّاة؛ وتبقى الصفوف القديمة لأثر التدقيق.
الحتميّة (V-6)
نفس مصفوفة الاستجابات + نفس إصدارات الاعتماديّات المثبّتة (requirements.txt) + نفس CONFIG_VERSION
تُنتج صعوبات متطابقة بايتاً ببايت, فملاءمة girth بِـ MML لا تستخدم أيّ عشوائية. إن غيّرت إعداد
التقارب أو رفعت girth/numpy، فارفع CONFIG_VERSION في config.py (يُختم على كلّ صفّ سِجلّ تشغيل)
كي يكون أيّ تغيّر في الصعوبة عائداً لتغيّر إعداد، لا لتغيّر بيانات.
استكشاف الأخطاء
| العَرَض | السبب / الحلّ |
|---|---|
0 eligible items | لا سؤال بلغ ≥ 300 استجابة بعد (الخطوة 2)، أو العناصر المؤهَّلة منفردة في مهارتها الفرعية (تحتاج ≥ 2 لكلّ خليّة). انتظر بيانات أكثر. |
فشل استعادة pytest | انحرف إصدار اعتماديّة, أعِد التثبيت من requirements.txt المثبَّت؛ ولا تُجرِ معايرة حقيقية حتى ينجح. |
relation ... does not exist | جداول المعايرة غير مُهاجَرة إلى هذه القاعدة, شغّل pnpm --filter server prisma migrate deploy أوّلاً. |
infit_mnsq ≫ 1.5 على سؤال | السؤال لا يلائم النموذج, أرسِله لمراجعة المحتوى؛ وفكّر في استبعاده قبل التشغيل القادم بدل تقديم δ مضلّلة. |
| إعادة تشغيل لم تُغيّر δ سؤال | متوقَّع إن لم تهبط استجابات جديدة له منذ آخر إصدار, فالملاءمة حتميّة. |