elemes/scripts/_scan_quiz_issues.py

43 lines
1.4 KiB
Python
Raw Permalink Normal View History

feat(quiz): taksonomi key quiz per jenis + MCQ berbobot (partial credit) Fitur B — setiap jenis soal kini punya key sendiri sehingga model penilaiannya eksplisit, tidak ditebak dari isi konten: - ---QUIZ_MCQ--- pilihan ganda biner (tepat satu [x]; larang [N]) - ---QUIZ_WEIGHTED--- pilihan ganda berbobot ([N] >= 1, minimal satu [ ], bobot tertinggi unik = kunci; poin parsial) - ---FLASHCARD--- kartu hafalan tanpa penilaian (larang opsi) - legacy ---QUIZ_FLASHCARD--- / ---INITIAL_QUIZ--- tetap dibaca tanpa perubahan perilaku (regex opsi legacy sengaja tidak berubah — baris '- [10]' di konten lama BUKAN opsi); dihapus dari docs/template - ::diagnostic tetap kategori di dalam QUIZ_MCQ & tetap masuk skor resmi Model skor: rata-rata persentase per jenis yang ada, Math.round, lulus >= 75. statusString: "X/Y" bila hanya MCQ/legacy, "P/100" bila ada weighted, "completed" bila hanya flashcard (auto-lulus). Satu sesi/ attempt untuk semua jenis; soal flat urut dokumen. Parser (backend & frontend mirror, ekstraksi multi-blok per key urut dokumen via loop marker-start paling awal; id q-N deterministik): - lesson_service.py: regex [ xX]?[0-9]* utk key baru, _iter_option_lines kind-aware, _validate_quiz_options per kind, _parse_flashcards(kind, id_offset), _extract_quiz_sections + guard loop maju (marker end hilang tidak lagi infinite loop), get_lesson_quiz_total -> 100 bila ada weighted (round-trip CSV "P/100") - parseFlashcards.ts / index.ts / detectActiveTabs.ts / extractSections.ts: mirror perilaku; heading soal <h3> kini fence-aware (fix fixture stale) Payload attempt baru: quiz_type, earned_points, question_max_points — opsional di validasi backend supaya klien lama tetap diterima; poin di-recompute dari konten saat resume/review. UI: QuizTab skor via statusString + alert berbasis percent, badge weighted (✓ 10 / 🟡 7/10 / ✗ 0) di review, preview guru badge bobot + "Berbobot (total N)", laporan guru badge MCQ/Bobot + tooltip, CSV progress 2 kolom baru per lesson (_quiz_mcq, _quiz_weighted), template editor konten QUIZ_MCQ/Quiz Bobot/Flashcard. Tooling & docs: scripts/convert_quiz_keys.py (konversi legacy -> key baru per run jenis, idempoten, verifikasi parse identik; examples/ content dikonversi), scanner mendukung semua key, docs/07 ditulis ulang, docs/15 direvisi ke desain final, README/documentation/docs 02/04/09 diperbarui. Tes: backend 444 lulus (+33: validasi ketat per key, urutan dokumen, multi-blok, zero regression, field selalu di-emit), frontend 249 lulus (+13 parser key baru, +11 model skor), parity fixture diregenerasi, svelte-check 0 error, vite build OK.
2026-10-25 20:16:56 +07:00
"""Pindai konten markdown & laporkan masalah kuis (alat bantu dev, ad-hoc).
Mendukung SEMUA key quiz (baru + legacy):
---QUIZ_MCQ--- / ---QUIZ_WEIGHTED--- / ---FLASHCARD--- / ---QUIZ_FLASHCARD---
Dipakai manual dari root repo:
python scripts/_scan_quiz_issues.py [CONTENT_DIR]
"""
import sys, glob, os
CONTENT_DIR = sys.argv[1] if len(sys.argv) > 1 else '/home/a2nr/lms-dev/elemes/content'
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from services.lesson_service import ( # noqa: E402
_parse_flashcards,
_extract_quiz_sections,
QUIZ_SECTION_SPECS,
)
issues = 0
feat(quiz): taksonomi key quiz per jenis + MCQ berbobot (partial credit) Fitur B — setiap jenis soal kini punya key sendiri sehingga model penilaiannya eksplisit, tidak ditebak dari isi konten: - ---QUIZ_MCQ--- pilihan ganda biner (tepat satu [x]; larang [N]) - ---QUIZ_WEIGHTED--- pilihan ganda berbobot ([N] >= 1, minimal satu [ ], bobot tertinggi unik = kunci; poin parsial) - ---FLASHCARD--- kartu hafalan tanpa penilaian (larang opsi) - legacy ---QUIZ_FLASHCARD--- / ---INITIAL_QUIZ--- tetap dibaca tanpa perubahan perilaku (regex opsi legacy sengaja tidak berubah — baris '- [10]' di konten lama BUKAN opsi); dihapus dari docs/template - ::diagnostic tetap kategori di dalam QUIZ_MCQ & tetap masuk skor resmi Model skor: rata-rata persentase per jenis yang ada, Math.round, lulus >= 75. statusString: "X/Y" bila hanya MCQ/legacy, "P/100" bila ada weighted, "completed" bila hanya flashcard (auto-lulus). Satu sesi/ attempt untuk semua jenis; soal flat urut dokumen. Parser (backend & frontend mirror, ekstraksi multi-blok per key urut dokumen via loop marker-start paling awal; id q-N deterministik): - lesson_service.py: regex [ xX]?[0-9]* utk key baru, _iter_option_lines kind-aware, _validate_quiz_options per kind, _parse_flashcards(kind, id_offset), _extract_quiz_sections + guard loop maju (marker end hilang tidak lagi infinite loop), get_lesson_quiz_total -> 100 bila ada weighted (round-trip CSV "P/100") - parseFlashcards.ts / index.ts / detectActiveTabs.ts / extractSections.ts: mirror perilaku; heading soal <h3> kini fence-aware (fix fixture stale) Payload attempt baru: quiz_type, earned_points, question_max_points — opsional di validasi backend supaya klien lama tetap diterima; poin di-recompute dari konten saat resume/review. UI: QuizTab skor via statusString + alert berbasis percent, badge weighted (✓ 10 / 🟡 7/10 / ✗ 0) di review, preview guru badge bobot + "Berbobot (total N)", laporan guru badge MCQ/Bobot + tooltip, CSV progress 2 kolom baru per lesson (_quiz_mcq, _quiz_weighted), template editor konten QUIZ_MCQ/Quiz Bobot/Flashcard. Tooling & docs: scripts/convert_quiz_keys.py (konversi legacy -> key baru per run jenis, idempoten, verifikasi parse identik; examples/ content dikonversi), scanner mendukung semua key, docs/07 ditulis ulang, docs/15 direvisi ke desain final, README/documentation/docs 02/04/09 diperbarui. Tes: backend 444 lulus (+33: validasi ketat per key, urutan dokumen, multi-blok, zero regression, field selalu di-emit), frontend 249 lulus (+13 parser key baru, +11 model skor), parity fixture diregenerasi, svelte-check 0 error, vite build OK.
2026-10-25 20:16:56 +07:00
for f in glob.glob(os.path.join(CONTENT_DIR, '**', '*.md'), recursive=True):
txt = open(f, encoding='utf-8').read()
feat(quiz): taksonomi key quiz per jenis + MCQ berbobot (partial credit) Fitur B — setiap jenis soal kini punya key sendiri sehingga model penilaiannya eksplisit, tidak ditebak dari isi konten: - ---QUIZ_MCQ--- pilihan ganda biner (tepat satu [x]; larang [N]) - ---QUIZ_WEIGHTED--- pilihan ganda berbobot ([N] >= 1, minimal satu [ ], bobot tertinggi unik = kunci; poin parsial) - ---FLASHCARD--- kartu hafalan tanpa penilaian (larang opsi) - legacy ---QUIZ_FLASHCARD--- / ---INITIAL_QUIZ--- tetap dibaca tanpa perubahan perilaku (regex opsi legacy sengaja tidak berubah — baris '- [10]' di konten lama BUKAN opsi); dihapus dari docs/template - ::diagnostic tetap kategori di dalam QUIZ_MCQ & tetap masuk skor resmi Model skor: rata-rata persentase per jenis yang ada, Math.round, lulus >= 75. statusString: "X/Y" bila hanya MCQ/legacy, "P/100" bila ada weighted, "completed" bila hanya flashcard (auto-lulus). Satu sesi/ attempt untuk semua jenis; soal flat urut dokumen. Parser (backend & frontend mirror, ekstraksi multi-blok per key urut dokumen via loop marker-start paling awal; id q-N deterministik): - lesson_service.py: regex [ xX]?[0-9]* utk key baru, _iter_option_lines kind-aware, _validate_quiz_options per kind, _parse_flashcards(kind, id_offset), _extract_quiz_sections + guard loop maju (marker end hilang tidak lagi infinite loop), get_lesson_quiz_total -> 100 bila ada weighted (round-trip CSV "P/100") - parseFlashcards.ts / index.ts / detectActiveTabs.ts / extractSections.ts: mirror perilaku; heading soal <h3> kini fence-aware (fix fixture stale) Payload attempt baru: quiz_type, earned_points, question_max_points — opsional di validasi backend supaya klien lama tetap diterima; poin di-recompute dari konten saat resume/review. UI: QuizTab skor via statusString + alert berbasis percent, badge weighted (✓ 10 / 🟡 7/10 / ✗ 0) di review, preview guru badge bobot + "Berbobot (total N)", laporan guru badge MCQ/Bobot + tooltip, CSV progress 2 kolom baru per lesson (_quiz_mcq, _quiz_weighted), template editor konten QUIZ_MCQ/Quiz Bobot/Flashcard. Tooling & docs: scripts/convert_quiz_keys.py (konversi legacy -> key baru per run jenis, idempoten, verifikasi parse identik; examples/ content dikonversi), scanner mendukung semua key, docs/07 ditulis ulang, docs/15 direvisi ke desain final, README/documentation/docs 02/04/09 diperbarui. Tes: backend 444 lulus (+33: validasi ketat per key, urutan dokumen, multi-blok, zero regression, field selalu di-emit), frontend 249 lulus (+13 parser key baru, +11 model skor), parity fixture diregenerasi, svelte-check 0 error, vite build OK.
2026-10-25 20:16:56 +07:00
if not any(start in txt for _, start, _ in QUIZ_SECTION_SPECS):
continue
feat(quiz): taksonomi key quiz per jenis + MCQ berbobot (partial credit) Fitur B — setiap jenis soal kini punya key sendiri sehingga model penilaiannya eksplisit, tidak ditebak dari isi konten: - ---QUIZ_MCQ--- pilihan ganda biner (tepat satu [x]; larang [N]) - ---QUIZ_WEIGHTED--- pilihan ganda berbobot ([N] >= 1, minimal satu [ ], bobot tertinggi unik = kunci; poin parsial) - ---FLASHCARD--- kartu hafalan tanpa penilaian (larang opsi) - legacy ---QUIZ_FLASHCARD--- / ---INITIAL_QUIZ--- tetap dibaca tanpa perubahan perilaku (regex opsi legacy sengaja tidak berubah — baris '- [10]' di konten lama BUKAN opsi); dihapus dari docs/template - ::diagnostic tetap kategori di dalam QUIZ_MCQ & tetap masuk skor resmi Model skor: rata-rata persentase per jenis yang ada, Math.round, lulus >= 75. statusString: "X/Y" bila hanya MCQ/legacy, "P/100" bila ada weighted, "completed" bila hanya flashcard (auto-lulus). Satu sesi/ attempt untuk semua jenis; soal flat urut dokumen. Parser (backend & frontend mirror, ekstraksi multi-blok per key urut dokumen via loop marker-start paling awal; id q-N deterministik): - lesson_service.py: regex [ xX]?[0-9]* utk key baru, _iter_option_lines kind-aware, _validate_quiz_options per kind, _parse_flashcards(kind, id_offset), _extract_quiz_sections + guard loop maju (marker end hilang tidak lagi infinite loop), get_lesson_quiz_total -> 100 bila ada weighted (round-trip CSV "P/100") - parseFlashcards.ts / index.ts / detectActiveTabs.ts / extractSections.ts: mirror perilaku; heading soal <h3> kini fence-aware (fix fixture stale) Payload attempt baru: quiz_type, earned_points, question_max_points — opsional di validasi backend supaya klien lama tetap diterima; poin di-recompute dari konten saat resume/review. UI: QuizTab skor via statusString + alert berbasis percent, badge weighted (✓ 10 / 🟡 7/10 / ✗ 0) di review, preview guru badge bobot + "Berbobot (total N)", laporan guru badge MCQ/Bobot + tooltip, CSV progress 2 kolom baru per lesson (_quiz_mcq, _quiz_weighted), template editor konten QUIZ_MCQ/Quiz Bobot/Flashcard. Tooling & docs: scripts/convert_quiz_keys.py (konversi legacy -> key baru per run jenis, idempoten, verifikasi parse identik; examples/ content dikonversi), scanner mendukung semua key, docs/07 ditulis ulang, docs/15 direvisi ke desain final, README/documentation/docs 02/04/09 diperbarui. Tes: backend 444 lulus (+33: validasi ketat per key, urutan dokumen, multi-blok, zero regression, field selalu di-emit), frontend 249 lulus (+13 parser key baru, +11 model skor), parity fixture diregenerasi, svelte-check 0 error, vite build OK.
2026-10-25 20:16:56 +07:00
sections, _ = _extract_quiz_sections(txt)
for kind, raw in sections:
if not raw.strip():
continue
try:
cards = _parse_flashcards(raw, kind=kind)
except ValueError as e:
print(f'[RAISE] {f}: {e}')
issues += 1
feat(quiz): taksonomi key quiz per jenis + MCQ berbobot (partial credit) Fitur B — setiap jenis soal kini punya key sendiri sehingga model penilaiannya eksplisit, tidak ditebak dari isi konten: - ---QUIZ_MCQ--- pilihan ganda biner (tepat satu [x]; larang [N]) - ---QUIZ_WEIGHTED--- pilihan ganda berbobot ([N] >= 1, minimal satu [ ], bobot tertinggi unik = kunci; poin parsial) - ---FLASHCARD--- kartu hafalan tanpa penilaian (larang opsi) - legacy ---QUIZ_FLASHCARD--- / ---INITIAL_QUIZ--- tetap dibaca tanpa perubahan perilaku (regex opsi legacy sengaja tidak berubah — baris '- [10]' di konten lama BUKAN opsi); dihapus dari docs/template - ::diagnostic tetap kategori di dalam QUIZ_MCQ & tetap masuk skor resmi Model skor: rata-rata persentase per jenis yang ada, Math.round, lulus >= 75. statusString: "X/Y" bila hanya MCQ/legacy, "P/100" bila ada weighted, "completed" bila hanya flashcard (auto-lulus). Satu sesi/ attempt untuk semua jenis; soal flat urut dokumen. Parser (backend & frontend mirror, ekstraksi multi-blok per key urut dokumen via loop marker-start paling awal; id q-N deterministik): - lesson_service.py: regex [ xX]?[0-9]* utk key baru, _iter_option_lines kind-aware, _validate_quiz_options per kind, _parse_flashcards(kind, id_offset), _extract_quiz_sections + guard loop maju (marker end hilang tidak lagi infinite loop), get_lesson_quiz_total -> 100 bila ada weighted (round-trip CSV "P/100") - parseFlashcards.ts / index.ts / detectActiveTabs.ts / extractSections.ts: mirror perilaku; heading soal <h3> kini fence-aware (fix fixture stale) Payload attempt baru: quiz_type, earned_points, question_max_points — opsional di validasi backend supaya klien lama tetap diterima; poin di-recompute dari konten saat resume/review. UI: QuizTab skor via statusString + alert berbasis percent, badge weighted (✓ 10 / 🟡 7/10 / ✗ 0) di review, preview guru badge bobot + "Berbobot (total N)", laporan guru badge MCQ/Bobot + tooltip, CSV progress 2 kolom baru per lesson (_quiz_mcq, _quiz_weighted), template editor konten QUIZ_MCQ/Quiz Bobot/Flashcard. Tooling & docs: scripts/convert_quiz_keys.py (konversi legacy -> key baru per run jenis, idempoten, verifikasi parse identik; examples/ content dikonversi), scanner mendukung semua key, docs/07 ditulis ulang, docs/15 direvisi ke desain final, README/documentation/docs 02/04/09 diperbarui. Tes: backend 444 lulus (+33: validasi ketat per key, urutan dokumen, multi-blok, zero regression, field selalu di-emit), frontend 249 lulus (+13 parser key baru, +11 model skor), parity fixture diregenerasi, svelte-check 0 error, vite build OK.
2026-10-25 20:16:56 +07:00
continue
for c in cards:
if c['type'] == 'mcq' and len(c.get('options') or []) < 2:
label = (c.get('question') or c.get('front') or '')[:60]
print(f'[<2 OPSI] {f}: {label!r} opsi={len(c.get("options") or [])}')
issues += 1
print('ISSUES:', issues)