Skip to content

Add PM-L1 (Preparatory Module Level 1): DPS/lessons + assessment + mock exam - #428

Merged
sg2499 merged 2 commits into
mainfrom
feature/pm-l1-bridge-replica-dps-2026-08-04
Aug 4, 2026
Merged

Add PM-L1 (Preparatory Module Level 1): DPS/lessons + assessment + mock exam#428
sg2499 merged 2 commits into
mainfrom
feature/pm-l1-bridge-replica-dps-2026-08-04

Conversation

@sg2499

@sg2499 sg2499 commented Aug 4, 2026

Copy link
Copy Markdown
Owner

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.

sg2499 added 2 commits August 4, 2026 17:26
…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.
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
math-path-platform Ready Ready Preview Aug 4, 2026 12:53pm

@sg2499
sg2499 merged commit 827cef5 into main Aug 4, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant