Files
MathApp/docs/content-authoring.md
Zareian ab5e53730a Audit fixes 2026-10-03: seed keeps admin edits, plan cache, refresh limit, payment and tutor guards
- seed_content no longer undoes admin work: a SeedRecord ledger (content/seed_sync.py)
  stops deleted/renamed/re-worded content from being recreated and keeps admin-edited
  lesson text; new rows in the seed files still arrive (migration content.0010)
- plan list cache is invalidated on Plan save (receiver used a stale 'v1' key)
- token refresh per-IP cap 30/hour -> 600/hour (TOKEN_REFRESH_IP_RATE)
- a failed gateway result only changes a pending order (forged NOK no longer
  overwrites refunded/canceled)
- tutor gets the correct answer only after the student has answered
- support reply notification moved to a TicketMessage post_save signal
- global daily cap on paid AI questions (AI_GLOBAL_DAILY_MAX, default 3000)
- Bazaar verify uses only the order's SKU; bazaar orders require a SKU
- /api/health warns on the committed signing key; deploy.sh saves local commits;
  400 for bad ?unit= / ?period=; account delete keeps the referrer's Conversion
- earlier today: no stub answers outside DEBUG, ?grade= removed, logged fallbacks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 16:05:27 +03:30

129 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ساختن و ویرایشِ محتوا
هدفِ این مسیر یک جمله است: **محتوا باید بدونِ توسعه‌دهنده قابلِ ویرایش باشد.**
اجرای یک management command روی SSH آخرین راهِ چاره است، نه روالِ کار.
## محتوا کجا زندگی می‌کند
`content/seed/` کلِ برنامه‌ی درسی و بانکِ سوال را به‌صورتِ JSONِ کامیت‌شده نگه
می‌دارد:
| فایل | محتوا |
|---|---|
| `curriculum.json` | ۸ پایه، ۵۸ فصل، ۲۶۳ نود، ۲٬۶۱۷ درس |
| `questions.json` | ۲٬۵۴۵ سوالِ چندگزینه‌ای |
`deploy.sh` در هر انتشار `seed_content` را اجرا می‌کند، پس یک سرورِ تازه خودش
را با یک فرمان پر می‌کند — **بدونِ Qdrant، بدونِ اعتبارِ API، بدونِ نشستِ SSH.**
> **کد و محتوا دو مسیرِ جدا دارند.** `db.sqlite3` در gitignore است. سوالی که
> فقط در دیتابیسِ محلیِ شماست به سرور نمی‌رسد مگر از راهِ
> `seed_content --export --only questions` و کامیتِ `content/seed/questions.json`.
> پوش کردنِ کد به‌تنهایی محتوا را نمی‌برد.
دو رفتارِ `seed_content` که دانستنشان لازم است:
- **idempotent است و هرگز چیزی پاک نمی‌کند.** هر چه در فایل هست ساخته یا
به‌روز می‌شود؛ هر چه نیست دست‌نخورده می‌ماند. پس یک فایلِ ناقص محتوای سرور را
از بین نمی‌برد.
- **سوال‌ها با مسیرِ عنوان به درس وصل می‌شوند** (پایه › فصل › نود › درس)، نه با
کلیدِ اصلی. شناسه‌ها بینِ هر دو دیتابیس فرق دارند و اتصال با id، سوال‌ها را
بی‌صدا به درسِ اشتباه می‌چسباند.
- **کارِ ادمین را برنمی‌گرداند** (از ۲۰۲۶-۱۰-۰۳، `content/seed_sync.py`). هر چه
سید یک بار تحویل داده در دفترِ `SeedRecord` ثبت می‌شود. اگر ادمین بعداً سوالی را
پاک کند، صورتش را اصلاح کند یا نامِ فصل و درسی را عوض کند، دیپلوی آن را دوباره
نمی‌سازد. متنِ توضیحِ درس فقط وقتی از فایل به‌روز می‌شود که در ادمین دست نخورده
باشد. محتوای *تازه*ی فایل مثلِ قبل می‌رسد. پیش از این، هر دیپلوی سوالِ پاک‌شده را
برمی‌گرداند و تغییرِ نامِ یک فصل همه‌ی درس‌ها و سوال‌هایش را تکراری می‌کرد.
دیتابیسِ بی‌محتوا (سرورِ تازه) دفتر را نادیده می‌گیرد و همه‌چیز را از فایل می‌سازد.
نتیجه‌اش این است که برای برگرداندنِ چیزی که در ادمین پاک شده، همان‌جا دوباره بسازیدش
(یا ردیفِ `SeedRecord`ش را پاک کنید)؛ دیپلوی دیگر آن را برنمی‌گرداند.
`seed_content --export` فایل‌ها را از دیتابیسِ محلی بازنویسی می‌کند. اگر
دیتابیس سوالی نداشته باشد از بازنویسیِ یک `questions.json` غیرخالی خودداری
می‌کند (این قبلاً فایل را بی‌صدا پاک می‌کرد)؛ وقتی فقط برنامه‌ی درسی را
می‌خواهید `--export --only curriculum` بزنید.
## از پنلِ ادمین (روالِ عادی)
| کار | کجا |
|---|---|
| افزودن یا ویرایشِ **یک** سوال | `/admin/content/question/` → «افزودن» (انتخابِ درس با جستجو) |
| وارد کردنِ **فله‌ای** سوال | همان صفحه → «وارد کردنِ سوال‌ها از فایل» |
| گرفتنِ خروجیِ سوال‌ها | چند سوال را انتخاب کنید → اکشنِ «دریافتِ فایل» |
| وارد کردنِ **برنامه‌ی درسی** | `/admin/content/grade/` → «وارد کردنِ برنامه‌ی درسی از فایل» |
| گرفتنِ خروجیِ برنامه‌ی درسی | پایه‌ها را انتخاب کنید → اکشنِ «دریافتِ فایلِ برنامه‌ی درسی» |
| افزودنِ یک **پایه‌ی تازه** | `/admin/content/grade/add/` — عنوان به شکلِ «پایه چهارم»؛ شماره و id خودکار ۴ می‌شوند |
خروجیِ هر کدام **دقیقاً** همان قالبی‌ست که ورودی‌اش می‌پذیرد، پس چرخه‌ی
«بگیر → ویرایش کن → برگردان» بسته است و برای دیدنِ یک نمونه لازم نیست کسی
مستندات بخواند.
دو تضمین:
- **سوال‌ها:** فایلِ نامعتبر هیچ چیزی نمی‌نویسد — یا همه وارد می‌شوند یا هیچ‌کدام.
- **برنامه‌ی درسی:** چیزی پاک نمی‌شود، به همان دلیلِ بالا.
## ساختِ انبوهِ سوال از یک کتابِ تازه
وقتی کتابی تازه روی Qdrant آمده، مسیرِ رایگان این است:
```bash
# اسکلتِ برنامه‌ی درسی از فهرستِ خودِ کتاب (بدونِ هزینه)
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`](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 دیده می‌شوند. محتوای واقعی از
مسیرِ بالا می‌رود، نه از این فرمان.
```bash
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ِ قبلی و ساختِ دوباره
```