Skip to content

feat: add grading_style and sub_group columns, fix GradingHelper bug, decouple grading logic - #369

Open
Mucunguzi256 wants to merge 15 commits into
mainfrom
feat/grading-style-and-sub-group
Open

feat: add grading_style and sub_group columns, fix GradingHelper bug, decouple grading logic#369
Mucunguzi256 wants to merge 15 commits into
mainfrom
feat/grading-style-and-sub-group

Conversation

@Mucunguzi256

Copy link
Copy Markdown
Collaborator

Summary

Adds grading_style enum on standards and sub_group varchar on standards_link, fixes a GradingHelper underscore-prefix bug, and decouples report-card grading logic from hardcoded standard-name checks.

Depends on: #367 (prefill defaults principle), #368 (Toshi content-step delegation) — merge those first.

Changes

Schema (PR 1)

  • Migration: grading_style enum('aggregate','total_marks') nullable on standards, sub_group string nullable on standards_link
  • Standard model: added grading_style to $fillable
  • StandardLink model: added sub_group to $fillable

GradingHelper bug fix

  • levelTypeForStandard(): added str_starts_with($name, $keyword.'_') alongside existing space-prefix match. Bug: primary_lowernull because str_starts_with('primary_lower', 'primary ') is false. Now correctly matches underscore-prefixed variants.

Report-card grading decoupling

  • ReportCardsController: showAgg reads $standard->grading_style first; falls back to !$isNursery && !in_array($standardName, ['primary_lower']) when NULL — zero behavior change for rows where grading_style is still NULL.
  • ReportCardCommentService: new resolveGroup() method uses grading_style when set (total_marks'lower', aggregate'upper'); falls back to in_array name check when NULL. Optional ?Standard $standard parameter added to commentFor() and headTeacherCommentFor() — fully backward-compatible.

OnboardingEngine extension

  • saveStandards(): accepts optional sub_group key in $classes array, passed to StandardLink::firstOrCreate via array_filter (excludes NULL values from search/create array — SQL NULL ≠ NULL comparison).

Backfill command (PR 2)

  • kabale:backfill-grading-style Artisan command with --dry-run and --school= options
  • Maps: nursery/primary_lower → total_marks, primary/primary_upper/o-level/a-level → aggregate
  • Maps: primary_lower → sub_group='lower', primary_upper → sub_group='upper', others → NULL
  • Idempotent: skips already-correct values, fails gracefully for non-existent schools

Test results

Suite Tests Assertions
GradingHelperLevelTypeTest 13 PASS 13
GradingStyleResolutionTest 10 PASS 16
BackfillKabaleGradingStyleTest 10 PASS 31
ContentStepsTest (incl. 4 new sub_group) 48 PASS 97
Total 81 PASS 157

Backward compatibility

  • All new columns are nullable — no existing row is affected
  • All new parameters are optional — no existing caller needs changes
  • Fallback logic (when grading_style is NULL) produces identical behavior to the current hardcoded checks
  • No data migration required for any school other than Kabale (backfill command is school-scoped)

Known follow-ups (logged, not in this PR)

  • F1: Hardcoded teacher password security gap (all creation paths use literal "password") — needs its own fix
  • F2: Hardcoded 3-terms display in 2 blade templates — needs dynamic term-map builder

… principle

Add standing rule #17 to AGENTS.md: when a step's data model is genuinely
configurable, the UI should still prefill sensible real-world defaults rather
than presenting an empty form. Example: saveTerms accepts any number of terms,
but the UI should prefill 3 standard UNEB terms.

Add matching §7 (UI/UX design principles) to docs/onboarding-engine-plan.md
with a cross-link to AGENTS.md rule #17.

Flagged as 'documented, not yet implemented' in both places.
…nciple

Promotes AGENTS.md rule #17 ('Configurable, but never blank') into a
reusable skill document with concrete defaults per onboarding step,
implementation checklist, and cross-references.

- New: docs/onboarding-defaults-skill.md — the canonical skill doc,
  tracked in the repo so it's version-controlled and discoverable.
  Covers: core principle, why, known defaults per step (terms, classes,
  subjects, fees, academic year), implementation checklist, and status.
- Updated: AGENTS.md rule #17 — added pointer to the new skill doc.
- Also created local-only SKILL.md files in .agents/skills/,
  .claude/skills/, .cursor/skills/, .junie/skills/ for agent
  auto-discovery (these directories are gitignored and won't appear
  in the PR diff).

Approach chosen: dedicated docs/onboarding-defaults-skill.md rather
than extending DESIGN_SYSTEM.md, because DESIGN_SYSTEM.md is a
component API reference (props, CSS classes, migration patterns) and
a behavioral defaults principle is a different concern. The local
SKILL.md convention is established but gitignored — it supplements
agent auto-discovery without replacing the version-controlled doc.
Replace inline Standard::create, Section::firstOrCreate, StandardLink::firstOrCreate,
Subject::firstOrCreate, AcademicTerm::create/firstOrCreate, and FeesCategories::create/firstOrCreate
in both create-mode and complete-mode paths of AgentToshi::commitAll() with delegation
calls to OnboardingEngine::saveStandards(), saveSubjects(), saveTerms(), and saveFees().

This fixes two real, previously-live bugs:
1. Single-Standard mapping for mixed-level schools: the old code created ONE $phase Standard
   (named after $this->schoolType) and mapped ALL classes to it. For a mixed-level school
   (e.g. nursery + primary + o-level), subjects and fees were invisible to tiers that didn't
   match the one Standard. OnboardingEngine creates per-class tier Standards correctly.
2. SchoolCategorySeeder never ran for Toshi: canonical defaults (core subjects, Standard rows
   for each tier) were missing because the old code never called SchoolCategorySeeder.
   OnboardingEngine runs it when school_category is set.

Also adds feesForEngine() helper that transforms Toshi's string[] fee names into the
structured array format expected by OnboardingEngine::saveFees().

Test changes:
- ToshiCommitAllCompleteModeTest: updated to assert CORRECT per-class tier mapping
  (primary Standard for P1-P3) instead of old buggy single-Standard assertion
- ToshiContentStepDelegationTest: 5 new tests verifying delegation and bug fixes
  - toshi_creates_per_class_tier_standards_not_single_phase (Bug 1 regression)
  - toshi_delegates_subjects_to_engine (uses DB::table() to avoid Subject name accessor)
  - toshi_delegates_terms_to_engine
  - toshi_delegates_fees_to_engine_whole_school_spread (Bug 1 + pattern #7 regression)
  - toshi_complete_mode_also_delegates_to_engine
Reference research for future grading-system redesign, not yet implemented.
Documents three levels at different reform stages:
- O-Level (UCE): REFORMED, CA-weighted 20/80, letter grades A-E
- A-Level (UACE): NOT yet reformed, old points system still live
- Primary (PLE): traditional Division 1-4, purely exam-based

Also updates Current Status header with sub-grouping/grading_style next step.
Two follow-up items from the 2026-08-26 sub_group investigation:
F1. Security: all 3 teacher-creation paths hardcode literal "password"
    with no forced reset — real security gap, not fixed yet.
F2. Cosmetic: 2 blade templates hardcode 3-term name map instead of
    building from school's real AcademicTerm records — shows "-"
    for schools with different term names.
… decouple grading logic

PR 1: Schema + GradingHelper fix + report-card decoupling + OnboardingEngine extension

- Add migration: grading_style enum('aggregate','total_marks') nullable on
  standards, sub_group varchar nullable on standards_link
- Fix GradingHelper::levelTypeForStandard() bug: str_starts_with now
  also matches underscore-prefixed variants (primary_lower → 'primary')
- Decouple ReportCardsController showAgg logic: use grading_style when
  set, fall back to isNursery + in_array name check when NULL
- Decouple ReportCardCommentService: new resolveGroup() method uses
  grading_style when set (total_marks→'lower', aggregate→'upper'),
  falls back to in_array name check when NULL. Optional ?Standard
  parameter added to commentFor/headTeacherCommentFor for backward
  compatibility
- Extend OnboardingEngine::saveStandards(): optional 'sub_group' key in
  classes array, passed to StandardLink::firstOrCreate via array_filter
  to exclude NULL values (avoids SQL NULL!=NULL issues)
- All new columns are nullable, all new params are optional — existing
  callers unaffected

Tests:
- GradingHelperLevelTypeTest: 13 tests (underscore-prefix regression)
- GradingStyleResolutionTest: 10 tests (grading_style precedence, fallback)
- ContentStepsTest: 4 new sub_group tests (stored, null default, streams,
  mixed)
- Existing tests: ReportCardHeadTeacherCommentTest 8 PASS,
  ReportTotalExcludesNonContributingExamsTest 7 PASS,
  DashboardStudentCountExcludesInactiveTest 2 PASS
PR 2: KabaleSetGradingStyle Artisan command + tests

- New command: kabale:backfill-grading-style (--school=, --dry-run)
  - Backfills grading_style on standards: nursery/primary_lower →
    total_marks, primary/primary_upper/o-level/a-level → aggregate
  - Backfills sub_group on standards_link: primary_lower → 'lower',
    primary_upper → 'upper', others → NULL (no sub-group)
  - Idempotent: skips already-correct values
  - Fails gracefully for non-existent school IDs
- Step 6 (remove hardcoded in_array checks) is already done:
  ReportCardsController and ReportCardCommentService already use
  grading_style as primary source with in_array only as NULL fallback

Tests: BackfillKabaleGradingStyleTest 10 tests, 31 assertions
…d expanded follow-up items

- New session log: 2026-08-26 sub-group/grading_style implementation (two PRs)
- Expanded F1 (hardcoded teacher password) and F2 (hardcoded 3 terms) from truncated one-liners to full descriptions
Copilot AI lite review requested due to automatic review settings August 25, 2026 23:04

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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.

2 participants