Files
MathApp/docs/content-authoring.md

16 KiB
Raw Permalink Blame History

ساختن و ویرایشِ محتوا

هدفِ این مسیر یک جمله است: محتوا باید بدونِ توسعه‌دهنده قابلِ ویرایش باشد. اجرای یک management command روی SSH آخرین راهِ چاره است، نه روالِ کار.

محتوا کجا زندگی می‌کند

content/seed/ کلِ برنامه‌ی درسی و بانکِ سوال را به‌صورتِ JSONِ کامیت‌شده نگه می‌دارد:

فایل محتوا
curriculum.json ۱۰ پایه (سوم تا دوازدهم)، ۵ کتاب، ۸۹ فصل، ۱٬۱۴۵ جزیره، ۵٬۴۰۶ درس؛ با subject، متنِ درس، مثال، قدم‌ها و شکلِ درس (figures)
questions.json ۳۹٬۱۵۶ سوالِ چهارگزینه‌ای، بعضی با شکل (figure)

هر فصل باید subject داشته باشد (ریاضی، حسابان، هندسه، آمار و احتمال، گسسته)؛ فایلِ بی‌آن رد می‌شود. شکل‌ها (Lesson.figures، Question.figure) با rag.services.diagram.validate در ورود اعتبارسنجی می‌شوند و شکلِ نامعتبر کلِ فایل را رد می‌کند. هیچ LaTeX یا markdownی وارد نمی‌شود: content/plain_math.py متن را در ورود ساده می‌کند.

deploy.sh در هر انتشار seed_content را اجرا می‌کند، پس یک سرورِ تازه خودش را با یک فرمان پر می‌کند — بدونِ Qdrant، بدونِ اعتبارِ API، بدونِ نشستِ SSH.

کد و محتوا دو مسیرِ جدا دارند. دیتابیس در گیت نیست. سوالی که فقط در دیتابیسِ محلیِ شماست به سرور نمی‌رسد مگر از راهِ seed_content --export --only questions و کامیتِ content/seed/questions.json. پوش کردنِ کد به‌تنهایی محتوا را نمی‌برد.

دو رفتارِ seed_content که دانستنشان لازم است:

  • idempotent است و هرگز چیزی پاک نمی‌کند. هر چه در فایل هست ساخته یا به‌روز می‌شود؛ هر چه نیست دست‌نخورده می‌ماند. پس یک فایلِ ناقص محتوای سرور را از بین نمی‌برد.
  • سوال‌ها با مسیرِ عنوان به درس وصل می‌شوند (پایه › فصل › نود › درس)، نه با کلیدِ اصلی. شناسه‌ها بینِ هر دو دیتابیس فرق دارند و اتصال با id، سوال‌ها را بی‌صدا به درسِ اشتباه می‌چسباند.
  • کارِ ادمین را برنمی‌گرداند (از ۲۰۲۶-۱۰-۰۳، content/seed_sync.py). هر چه سید یک بار تحویل داده در دفترِ SeedRecord ثبت می‌شود. اگر ادمین بعداً سوالی را پاک کند، صورتش را اصلاح کند یا نامِ فصل و درسی را عوض کند، دیپلوی آن را دوباره نمی‌سازد. متنِ توضیحِ درس فقط وقتی از فایل به‌روز می‌شود که در ادمین دست نخورده باشد. محتوای تازهی فایل مثلِ قبل می‌رسد. پیش از این، هر دیپلوی سوالِ پاک‌شده را برمی‌گرداند و تغییرِ نامِ یک فصل همه‌ی درس‌ها و سوال‌هایش را تکراری می‌کرد. دیتابیسِ بی‌محتوا (سرورِ تازه) دفتر را نادیده می‌گیرد و همه‌چیز را از فایل می‌سازد. نتیجه‌اش این است که برای برگرداندنِ چیزی که در ادمین پاک شده، همان‌جا دوباره بسازیدش (یا ردیفِ SeedRecordش را پاک کنید)؛ دیپلوی دیگر آن را برنمی‌گرداند.
  • مثال، قدم‌ها و شکلِ درس فقط در خانه‌ی خالی نشانده می‌شوند. برای عوض کردنِ شکلِ درسی که روی سرور هست، figures آن درس را در ادمین خالی کنید و دوباره دیپلو کنید. شناسه‌ی درس روی سرور با لپ‌تاپ فرق دارد.
  • جزیره‌های تمرین («تمرین: …») استثنا هستند: گزینه‌های سوالِ موجودشان در جا (با همان شناسه) با فایل یکی می‌شود و سوالی که از فایل حذف شده و هنوز جوابی ندارد پاک می‌شود (seed_sync._refresh_practice). سوالِ دستیِ ادمین هرگز.

seed_content --export فایل‌ها را از دیتابیسِ محلی بازنویسی می‌کند. اگر دیتابیس سوالی نداشته باشد از بازنویسیِ یک questions.json غیرخالی خودداری می‌کند (این قبلاً فایل را بی‌صدا پاک می‌کرد)؛ وقتی فقط برنامه‌ی درسی را می‌خواهید --export --only curriculum بزنید.

از پنلِ ادمین (روالِ عادی)

کار کجا
افزودن یا ویرایشِ یک سوال /admin/content/question/ → «افزودن» (انتخابِ درس با جستجو)
وارد کردنِ فله‌ای سوال همان صفحه → «وارد کردنِ سوال‌ها از فایل»
گرفتنِ خروجیِ سوال‌ها چند سوال را انتخاب کنید → اکشنِ «دریافتِ فایل»
وارد کردنِ برنامه‌ی درسی /admin/content/grade/ → «وارد کردنِ برنامه‌ی درسی از فایل»
گرفتنِ خروجیِ برنامه‌ی درسی پایه‌ها را انتخاب کنید → اکشنِ «دریافتِ فایلِ برنامه‌ی درسی»
افزودنِ یک پایه‌ی تازه /admin/content/grade/add/ — عنوان به شکلِ «پایه چهارم»؛ شماره و id خودکار ۴ می‌شوند

خروجیِ هر کدام دقیقاً همان قالبی‌ست که ورودی‌اش می‌پذیرد، پس چرخه‌ی «بگیر → ویرایش کن → برگردان» بسته است و برای دیدنِ یک نمونه لازم نیست کسی مستندات بخواند.

دو تضمین:

  • سوال‌ها: فایلِ نامعتبر هیچ چیزی نمی‌نویسد — یا همه وارد می‌شوند یا هیچ‌کدام.
  • برنامه‌ی درسی: چیزی پاک نمی‌شود، به همان دلیلِ بالا.

ساختِ انبوهِ سوال از یک کتابِ تازه

وقتی کتابی تازه روی Qdrant آمده، مسیرِ رایگان این است:

# اسکلتِ برنامه‌ی درسی از فهرستِ خودِ کتاب (بدونِ هزینه)
python manage.py seed_curriculum_from_textbook --subject ریاضی --grade نهم --dry-run
python manage.py seed_curriculum_from_textbook --subject ریاضی --grade نهم

# جفت‌های (درس، مسئله‌ی تأییدشده) را برای سوال‌نویسی بیرون بده (بدونِ هزینه)
python manage.py generate_questions_from_chunks --subject ریاضی --grade نهم \
  --export /tmp/batches

--dry-run روی هر دو کار می‌کند و پیش از هر نوشتنی نشان می‌دهد چه ساخته خواهد شد.

خروجی چند فایلِ JSON است به‌همراهِ یک *-RULES.txt که عینِ همان پرامپتی را دارد که به مدلِ پولی می‌رفت — تا کیفیتِ دو مسیر از هم جدا نیفتد. سوال‌ها را با آن قواعد بنویسید و از «وارد کردنِ فله‌ای سوال» در جدولِ بالا واردشان کنید.

همان فرمان بدونِ --export کار را به مدلِ پولی می‌سپارد. این کار را نکنید مگر دلیلِ خاصی داشته باشید: اندازه‌گیری‌شده، گران‌تر است و کیفیتِ بهتری نمی‌دهد. جزئیات و هزینه‌های واقعی در extraction-playbook.md.

کیفیتِ محتوا

کتاب‌ها با یک مدلِ ویژن از تصویرِ صفحه خوانده می‌شوند، پس خطای استخراج از همان‌جا وارد می‌شود: رقمِ بدخوانده‌شده، جداکننده‌ی اعشارِ افتاده، راه‌حلی که به مسئله‌ی بغلی چسبیده. این‌ها در بازیابی دیده نمی‌شوند و فقط وقتی لو می‌روند که چیزی به متن اعتماد کند.

rag_verify_solutions هر مسئله‌ی استخراج‌شده را مستقلاً دوباره حل می‌کند و نتیجه را مقایسه می‌کند. LessonChunk.solution_verdict سه‌حالته است — agrees / disagrees / unjudgeable. ساختِ سوال فقط از chunkهای agrees می‌خواند.

دو درسی که گران تمام شدند و حالا در پرامپت‌ها کدگذاری شده‌اند:

  • رأیِ دوحالته غلط است. با دو حالت، هر «نمی‌توانم قضاوت کنم» تبدیل به «غلط» می‌شود و نرخِ خطا را حدودِ سه برابرِ واقعیت گزارش می‌کند. پیش از اعتماد به هر اجرای انبوه، چند نمونه را با چشم بخوانید.
  • ۳/۱۰۴ در این کتاب‌ها عددِ اعشاریِ ۳٫۱۰۴ است، نه کسرِ ۳ تقسیم بر ۱۰۴.

خودِ solution هرگز بازنویسی نمی‌شود — متنِ استخراج‌شده به‌عنوانِ سند می‌ماند و پاسخِ مستقل جداگانه در checked_answer ذخیره می‌شود.

solution_verdict='agrees' تضمینِ کامل نیست، فقط فیلترِ اول: چند بار خطای حسابِ خودِ کتاب در همین مرحله گرفته شده است.

بازخوردِ دانش‌آموز

زیرِ هر سوالِ آزمون و هر درس سه دکمه است: «خوب بود»، «خوب نبود» و «گزارشِ ایراد». در ادمین، فهرستِ سوال‌ها و درس‌ها ستونِ 👍/👎 دارد و «گزارش‌های سوال» و «گزارش‌های درس» صفِ بررسی‌اند. این‌ها نخستین جایی‌اند که سوالِ غلط یا مبهم پیدا می‌شود.

داده‌ی ساختگی برای توسعه

seed_test_data یک دیتاستِ کوچک و idempotent می‌سازد که همه‌ی اپ‌ها را پوشش می‌دهد. هرگز روی سروری که کاربرِ واقعی دارد اجرا نشود — ردیف‌هایی با پیشوندِ [SEED]/[DEMO] می‌سازد که در API دیده می‌شوند. محتوای واقعی از مسیرِ بالا می‌رود، نه از این فرمان.

python manage.py seed_test_data              # فقط با DEBUG=True اجرا می‌شود
python manage.py seed_test_data --force      # staging، با DEBUG=False
python manage.py seed_test_data --reset      # پاک کردنِ داده‌ی seedِ قبلی و ساختِ دوباره

سوالِ شکل‌دار (از ۲۰۲۶-۱۰-۱۴)

آزمونِ جزیره از هر درس یک سوال برمی‌دارد و اگر درس سوالِ شکل‌دار داشته باشد، همان را برمی‌دارد؛ پس افزودنِ سوالِ شکل‌دار، سوالِ بی‌شکلِ همان درس را از آزمون کنار می‌زند، بی‌آنکه چیزی پاک شود. برنامه این است که شکل‌ها و سوال‌های بیشتری ساخته و کلِ بانک بازبینی شود؛ این سازوکار همان را بی‌کارِ اضافه ممکن می‌کند.

python tools/figures/batch.py spec                                  # کاتالوگِ نوع‌های شکل
python tools/figures/batch.py export "پایه دوازدهم" "فصل ۲" هندسه --out f.json   # درس‌های فصل
python tools/figures/batch.py check batch.json                      # سخت‌گیرانه: آیتمِ حذف‌شده = رد
python tools/figures/batch.py merge batch.json                      # افزودن به questions.json

درس‌هایی که ذاتاً شکل ندارند (تعریف، گزاره، نقیض) را شکل نمی‌دهیم. بیضی، سهمی و هذلولی نوعِ جدا ندارند: با دو function (نیمه‌ی بالا و پایین) کشیده می‌شوند. معادله‌ی خط را به شکلِ ax + by = c بنویسید (y = -x/3 + 1 خوانده نمی‌شود و شکلِ بی‌خط درمی‌آید). بعد از check، شکل را حتماً رندر و با چشم ببینید؛ در رندرِ آزمایشیِ چند شکل در یک صفحه، به renderToString با identifierPrefix یکتا برای هر شکل نیاز است، وگرنه useId در همه‌ی شکل‌ها یکی می‌شود و clipPathها روی هم می‌افتند و منحنی‌ها نصفه دیده می‌شوند (خطای ابزارِ آزمایش، نه اپ). فضای سه‌بعدی: {"kind":"space",…} با نقطه، بردار، پاره‌خط، چندضلعی (صفحه)، جعبه و خط‌چین‌های تصویر؛ تصویرِ مایلِ ثابت.

شکل نباید جواب را لو بدهد

گزارشِ مالک (۲۰۲۶-۱۰-۱۵): سوالِ «نقطه‌ی A(۰، ۵، ۰) روی کدام محور است؟» شکلی داشت که نقطه را با همان مختصات روی محور نشان می‌داد؛ شکل جواب را می‌داد. قاعده‌ها برای ساختنِ هر سوالِ شکل‌دار:

  1. متنِ شکل (title، برچسب‌ها، label) جواب یا چیزی مساوی با آن نباشد. عنوان را خالی بگذارید یا کلی («شکل»)؛ عنوانِ «هرم با قاعده‌ی مربع» زیرِ سوالِ «مقطع چیست؟» و «دو دایره‌ی مماس» زیرِ «چند نقطه‌ی مشترک؟» جواب بود.
  2. چیزی را که سوال می‌پرسد نکشید. اگر متن همه‌ی داده را داده (معادله، مختصات) و شکل نتیجه را هم نشان می‌دهد، شکل کارِ دانش‌آموز را می‌کند. یا داده را فقط در شکل بگذارید (و متن آن را نگوید)، یا شکل را برنداشته سوال را بی‌شکل بگذارید. مثال‌های بد: نقطه‌ای که «روی دایره است یا نه» و روی دایره کشیده شده؛ «لوزی با یک زاویه‌ی قائمه چه نام دارد» با مربع کشیده‌شده؛ خطی که «وضعیتش نسبت به دایره» پرسیده می‌شود و مماس کشیده شده؛ «نمودار x = ۱ چیست» با همان خط.
  3. مقدار را روی مماس، drop و ابزارهایی که خودشان عدد می‌نویسند نگذارید (ابزارِ tangent «شیب = …» را چاپ می‌کند؛ مماس را با معادله‌ی خط بکشید).
  4. هر سوالِ شکل‌دار را قبل از ادغام یک بار «به‌جای دانش‌آموز» بخوانید: اگر فقط با نگاه به شکل، بی‌هیچ فکر، جواب می‌شود، دوباره بنویسید یا بی‌شکل بگذارید.

اصلاحِ سوالِ منتشرشده: tools/figures/apply_fixups.py (حذف و خالی‌کردنِ عنوان، هم روی seed هم با --db روی سرور، پس از pg_dump)؛ seed خودش چیزی را بازنویسی نمی‌کند. فایل tools/figures/server_fixups.json اصلاح‌های سوال‌ها و درس‌هایی است که پیش از بازسازیِ پایه‌های ۳ تا ۱۲ منتشر شده بودند (۹۳ حذف، ۵۸۵ اصلاحِ سوال، ۲۷۳۸ اصلاحِ درس)؛ پس از هر استقرارِ seed با apply_fixups.py --db روی سرور اجرا می‌شود.