Add PM-L1 (Preparatory Module Level 1): DPS/lessons + assessment + mock exam - #428
Merged
Merged
Conversation
…generator Product decision: a student can enter the platform via three separate paths -- YLM, Bridge Module, or PM-L1 -- and all three must land the student at the same competency level before Intermediate/Master Module. PM-L1's first 15 lessons therefore replicate Bridge Module's first 15 lessons exactly (concepts, complement techniques, target numbers, and difficulty progression), rather than PM-L1's own prior curriculum. Per explicit instruction, PM gets a fully dedicated question_engine package -- app/question_engine/pm/ (config.py, validators.py, operands.py, distractors.py, generator.py) -- mirroring how MM and IM already each own their own generator, instead of routing through YLM's shared fallback engine. This was a deliberate revert-and-redo: an earlier version of this change reused YLM's generator via its generic non-MM/IM fallback path, which worked, but meant a future change to YLM's engine could silently change PM's output (this already surfaced once during that approach: a stale lesson-number-keyed table in YLM's engine, never scoped to module_code, would have leaked YLM's own revision-schedule into PM lessons 7/12/13 by coincidence of lesson numbering). The dedicated package removes that coupling entirely -- YLM's engine is back to byte-identical with its pre-PM state, and PM's package imports nothing from ylm/mm/im. - backend/app/question_engine/pm/*.py (new): independently authored bead-movement classification, complement-of-5/10 base pools, and difficulty staging for PM. The only shared imports are option_utils.py and smart_distractors.py -- generic MCQ-option assembly and plausible-wrong-number math already used identically by YLM, MM, and IM before PM existed, not curriculum logic, so reusing them creates no cross-module coupling. Also owns 2D_FULL / 3D_HUNDREDS / 3D_FULL digit patterns for Bridge Lesson 2's double/triple-digit direct practice, native to PM from the start rather than bolted onto a shared file. - backend/app/seed/preparatory_module_l1_config.py (new): the 15-lesson, 75-DPS curriculum config, derived from a full read-through of the Bridge Module Excel source and its lesson image assets. Each lesson follows the complement ladder Bridge uses: 1-4 (SB+BB combined per number), then 5 (BB only), then paired 6&7 and 8&9. - backend/app/seed/seed_preparatory_module.py (new): idempotent seed (Module PM -> Level PM-L1 -> 15 Lessons -> 75 DPS + DPSSections), following the same ensure_*() pattern as seed_ylm_phase1.py. - backend/app/services/generation_service.py: adds build_pm_config_from_dps() and a PM branch in _is_dynamic_generator_supported() / generate_preview() / persist_question_set(), structured identically to (but not sharing code with) the existing MM/IM branches. Purely additive -- zero lines changed in any existing YLM/MM/IM code path. - backend/app/main.py: wires the new seed into the unconditional curriculum sync block (same treatment as Master/Intermediate Module). backend/tests/test_seed_preparatory_module.py (new): exercises the real pipeline end-to-end -- seed_preparatory_module.seed() followed by generation_service.generate_preview(), the exact call Learning Path Studio's Generate Preview button makes -- across all 75 DPS. Verifies question count, answer correctness, no negative answers, sufficient variety per sheet, multi-target coverage for the paired 6&7/8&9 lessons, and that generation_template metadata actually reflects the configured template. Also adds a static-analysis guard asserting question_engine/pm/*.py contains no import of question_engine.ylm/mm/im, so this isolation can never silently regress. Full existing suite (140 tests total) passes with zero regressions. Scope: DPS/lesson generation only, per explicit instruction. Assessment and mock workflows for PM-L1 are intentionally not touched here.
Per explicit instruction, this follows the exact architecture MM/IM already use for mocks and assessments, differing only in section content: PM-L1 gets 2 sections (Section 1 - Addition, Section 2 - Subtraction) covering every addition/subtraction pattern taught across its 15 lessons, flat 1 mark per question for both workflows -- PM has no Skill Stacker/Concept Drill equivalent, so none of IM's weighting machinery applies. Number of questions and exam duration remain admin-controlled, same as every other module. Per the "no shared curriculum logic between modules" instruction established earlier in this branch, PM's mock section registry and question collector live in their own dedicated file (pm_competition_mock_generation_service.py), independently authored, calling PM's own question_engine/pm generator directly. The only imports from the MM/IM file are two generic, non-curriculum distribution utilities already documented as shared across "every current and future MM/IM level" (_RedistributeSectionCounts, _DenseSectionNumbering) -- everything describing PM's actual curriculum (section definitions, concept pools, question construction) is PM's own code. - backend/app/services/pm_competition_mock_generation_service.py (new): PM_COMPETITION_LEVEL_REGISTRY (PM-L1 -> 2 sections, addition and subtraction concept pools spanning direct add/less across 1D/2D/3D patterns plus complement-of-5 and complement-of-10 for every target 1-9), PmCompetitionLevelConfig(), and CollectPmCompetitionSectionLockedQuestions() -- PM's counterpart to _CollectMmCompetitionSectionLockedQuestions / _CollectImCompetitionSectionLockedQuestions, simpler by design since PM has no challenge-lesson escalation or freshness-window dedup. Regression note: the subtraction concept pool deliberately has no standalone "Round Hundreds" entry, unlike the addition pool. A round-hundred base (100, 200, ...) always has ones-digit 0, and DIRECT_SUB_ALLOWED[0] in question_engine/pm/validators.py is deliberately empty -- subtracting a single digit from a number ending in 0 requires a borrow across places, not a direct abacus move. This matches the real seeded curriculum exactly: Lesson 2 DPS4 (preparatory_module_l1_config.py) configures 3D_HUNDREDS with operation_focus="ADD_LESS", not subtraction-only, for exactly this reason. An earlier draft of this pool split it into symmetric Addition/Subtraction entries and the subtraction one produced zero valid questions -- caught by a new test that generates one question from every concept-pool entry independently. - backend/app/services/competition_mock_generation_service.py: adds _IsPreparatoryModule() (mirroring _IsMasterModule/ _IsIntermediateModule) and a PM branch in each of the three shared mock dispatch points -- _CollectGeneratedQuestions (early-return to the new collector, same shape as the IM branch), CompetitionMockSectionPlan (PM_2_SECTION_COMPETITION_MOCK structure), and GenerateCompetitionMockDraft's default question-count/duration ternaries. marks_per_question is already hardcoded flat at 1 for every module in this file, so PM's "1 mark per question" mock requirement needed no further change there. - backend/app/services/assessment_blueprint_service.py: adds "PM" to SECTION_WISE_ASSESSMENT_MODULES and _SECTION_WISE_REGISTRIES (pointing at PM_COMPETITION_LEVEL_REGISTRY), and extends validate_section_distribution()'s MM-only "must total exactly 100 questions" branch to also cover PM, since PM is flat-1-mark like MM with no concept-weighted sections. - backend/app/services/assessment_engine_service.py: adds a literal duplicate of the section-wise module set/registries (existing precedent to avoid a circular import), PmSectionRegistryConfig() and _GeneratePmAssessmentBatch() (PM's counterparts to the MM/IM registry-driven config builders and batch generators), a PM branch in GenerateAssessmentVersion's section-wise question generation loop and its marks-mode branch (new "PM_FLAT" mode, same treatment as MM_FLAT), and extends ResolvedAssessmentQuestionMark() and the post-generation marks_per_question assignment to treat PM as flat 1 mark like MM. - frontend/app/admin/competition/mock-studio/page.tsx: adds "PM" to CompetitionMockSupportedModuleCodes (previously PM's mock exams were gated out of the Create Mock form entirely) and an IsSelectedPreparatoryModule branch supplying PM's default question count (60) and duration (30 min), matching the backend defaults. - frontend/app/admin/assessment-blueprints/page.tsx: marksModeLabel() and one inline helper-text ternary now label PM as "Flat (1 Each)" / "Every Question Is 1 Mark" instead of falling through to the generic "Auto-Balanced" copy meant for YLM. The actual section-wise distribution UI (isSectionWiseLevel, per-section isWeighted) needed no changes -- it already reads those flags from the backend response generically rather than branching on module code. backend/tests/test_pm_competition_mock_and_assessment.py (new): registry shape, per-concept generation smoke test (the regression guard for the round-hundreds bug above), CollectPmCompetitionSectionLockedQuestions even-split + no-duplicate-operands + flat-1-mark checks, CompetitionMockSectionPlan and GenerateCompetitionMockDraft end-to-end through the real seeded curriculum, is_section_wise_module/ section_marks_metadata flat-marks checks, validate_section_distribution 100-question-total enforcement and missing-section rejection, and a full create_blueprint(status="PUBLISHED") -> GenerateAssessmentVersion run asserting 100 persisted questions, 100 total marks, 1 mark each, both section titles present, and lessonGroups grouping by section in the version payload. Full suite (150 tests total, 140 existing + 10 new) passes with zero regressions. Not yet pushed -- accumulating on this branch per instruction to push everything (DPS/lessons + assessment + mock) together once complete.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Full PM-L1 build: 15 lessons/75 DPS replicating Bridge Module's first 15 lessons, dedicated question_engine/pm package, competition mock exam, and section-wise assessment (Addition/Subtraction, flat 1 mark). 150/150 backend tests pass. See commit messages for full detail.