fix financial and certificates
This commit is contained in:
@@ -0,0 +1,764 @@
|
||||
# Strong Grading System Plan
|
||||
|
||||
## Purpose
|
||||
|
||||
This document defines a stronger grading-system plan for the Alrahma Sunday School API. It is intentionally a planning document, not an implementation patch.
|
||||
|
||||
The goal is to move the grading system from a functional calculator into a fair, explainable, auditable, backward-compatible, and policy-driven gradebook.
|
||||
|
||||
The current system already has a useful foundation: a centralized semester score service, separated score-entry flows, grading locks, and missing-score override tracking. However, the current scoring model still allows too much ambiguity around blank scores, missing work, validation, and finalization.
|
||||
|
||||
The strongest first move is not to redesign every formula. The strongest first move is to kill ambiguity while keeping old data readable and old calculations explainable.
|
||||
|
||||
---
|
||||
|
||||
## Core Principles
|
||||
|
||||
A strong grading system must be:
|
||||
|
||||
1. **Fair**
|
||||
Missing work must not accidentally improve a student's grade.
|
||||
|
||||
2. **Explainable**
|
||||
Every final score must be traceable to category scores, weights, attendance, exam score, and policy rules.
|
||||
|
||||
3. **Auditable**
|
||||
Locked or finalized scores must preserve the exact inputs and formula used at the time.
|
||||
|
||||
4. **Policy-driven**
|
||||
Grading policy should live in services, configuration, and grading profiles, not scattered controller logic.
|
||||
|
||||
5. **Safe**
|
||||
Invalid scores must be rejected before they reach averages or semester calculations.
|
||||
|
||||
6. **Backward-compatible**
|
||||
Old data must remain readable, displayable, and explainable under the legacy policy that created it.
|
||||
|
||||
7. **Additive, not destructive**
|
||||
The new grading system must be introduced beside the old one first. Existing historical scores must not be silently recalculated, reclassified, hidden, or overwritten.
|
||||
|
||||
---
|
||||
|
||||
## Non-Negotiable Backward Compatibility Requirement
|
||||
|
||||
The system must preserve old data display.
|
||||
|
||||
Historical grades must remain visible exactly as they were calculated under the legacy system. The new strong grading system must not reinterpret old blanks, old averages, or old semester scores as if they were created under the new policy.
|
||||
|
||||
This is the hard rule:
|
||||
|
||||
```text
|
||||
Old data must remain readable, displayable, and explainable exactly as it was calculated under the legacy system.
|
||||
```
|
||||
|
||||
The new system must support two modes:
|
||||
|
||||
| Mode | Purpose | Formula Source | Data Behavior |
|
||||
|---|---|---|---|
|
||||
| Legacy display mode | Show old/historical grades | Existing stored semester values and legacy formula | Do not reinterpret blanks or statuses |
|
||||
| Strong scoring mode | Calculate future grades under stronger rules | New scoring service/profile | Uses statuses, max points, snapshots, and finalization validation |
|
||||
|
||||
Legacy records must show stored values from the existing `semester_scores` table, such as:
|
||||
|
||||
```text
|
||||
homework_avg
|
||||
quiz_avg
|
||||
project_avg
|
||||
participation_score
|
||||
attendance_score
|
||||
ptap_score
|
||||
midterm_exam_score
|
||||
final_exam_score
|
||||
semester_score
|
||||
```
|
||||
|
||||
Legacy displays should include a visible label:
|
||||
|
||||
```text
|
||||
Legacy Calculation
|
||||
```
|
||||
|
||||
And a short explanation:
|
||||
|
||||
```text
|
||||
This score was calculated under the legacy grading policy. Blank scores were ignored in category averages, PTAP used legacy dynamic weighting, attendance included one-absence grace, and the semester exam used the legacy 60% weighting.
|
||||
```
|
||||
|
||||
This is not optional. Without this, the system risks showing old data through new rules and creating disputes that nobody can cleanly answer. Software should not gaslight the gradebook. Humans already do enough of that.
|
||||
|
||||
---
|
||||
|
||||
## Historical Data Protection Rules
|
||||
|
||||
Historical grades must not be automatically recalculated under the new scoring policy.
|
||||
|
||||
Rules:
|
||||
|
||||
1. Existing `semester_scores` rows stay in place.
|
||||
2. Existing historical rows are marked as legacy.
|
||||
3. Legacy rows are displayed from stored aggregate values.
|
||||
4. Legacy rows are not forced into the new status model.
|
||||
5. Legacy blanks are not automatically converted to `missing`.
|
||||
6. Legacy scores are not recalculated unless an authorized admin explicitly starts a recalculation job.
|
||||
7. If recalculation is ever allowed, the original legacy score must be preserved beside the new-policy result.
|
||||
|
||||
Recommended display if a historical record is later recalculated for comparison:
|
||||
|
||||
```text
|
||||
legacy_semester_score: 91.4
|
||||
new_policy_semester_score: 87.2
|
||||
displayed_score: 91.4 unless admin explicitly chooses otherwise
|
||||
```
|
||||
|
||||
The default behavior must protect the original displayed score.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Compatibility Metadata
|
||||
|
||||
Do not replace the existing `semester_scores` table. Keep it.
|
||||
|
||||
Add nullable metadata columns:
|
||||
|
||||
```text
|
||||
calculation_mode
|
||||
calculation_policy_version
|
||||
snapshot_id nullable
|
||||
```
|
||||
|
||||
Suggested values:
|
||||
|
||||
```text
|
||||
calculation_mode = legacy | strong
|
||||
calculation_policy_version = legacy_v1 | strong_v1
|
||||
```
|
||||
|
||||
Backfill existing rows as:
|
||||
|
||||
```text
|
||||
calculation_mode = legacy
|
||||
calculation_policy_version = legacy_v1
|
||||
snapshot_id = null
|
||||
```
|
||||
|
||||
New strong-system finalized rows should be saved with:
|
||||
|
||||
```text
|
||||
calculation_mode = strong
|
||||
calculation_policy_version = strong_v1
|
||||
snapshot_id = related semester_score_snapshots row
|
||||
```
|
||||
|
||||
This keeps old grade display stable while allowing new scores to become audit-proof.
|
||||
|
||||
---
|
||||
|
||||
## Display Resolver Requirement
|
||||
|
||||
Create a display resolver instead of forcing every grade display through the new scoring engine.
|
||||
|
||||
Recommended service:
|
||||
|
||||
```php
|
||||
App\Services\Grading\GradeCalculationDisplayResolver
|
||||
```
|
||||
|
||||
Responsibilities:
|
||||
|
||||
```text
|
||||
If calculation_mode is legacy:
|
||||
Load and display existing semester_scores fields.
|
||||
Show Legacy Calculation badge.
|
||||
Do not apply new status or profile rules.
|
||||
|
||||
If calculation_mode is strong:
|
||||
Load snapshot and/or strong calculation breakdown.
|
||||
Show Strong Calculation badge.
|
||||
Display status-aware category breakdown.
|
||||
```
|
||||
|
||||
The display layer must not recalculate legacy scores just to show them. It should show the stored values. Recalculating old scores on display is a terrible idea, and yet somehow tempting to developers who enjoy turning page loads into historical revisionism.
|
||||
|
||||
---
|
||||
|
||||
## Important Policy Clarification: Attendance Grace
|
||||
|
||||
The attendance grace rule is school policy.
|
||||
|
||||
The current formula effectively gives students one absence before attendance points are reduced:
|
||||
|
||||
```text
|
||||
attendance_score = ((total_days + 1 - absences) / total_days) * 100
|
||||
```
|
||||
|
||||
This is policy-compliant, but the formula hides the intent. It should be rewritten more explicitly:
|
||||
|
||||
```text
|
||||
grace_absences = 1
|
||||
effective_absences = max(0, absences - grace_absences)
|
||||
attendance_score = ((total_days - effective_absences) / total_days) * 100
|
||||
```
|
||||
|
||||
This keeps the same policy while making the rule clear. Future developers should not have to decode a `+1` and wonder whether it is a bug wearing a tiny policy hat.
|
||||
|
||||
---
|
||||
|
||||
## Phase 0: Lock the Grading Policy Before Writing Code
|
||||
|
||||
Before implementation, these policy decisions must be explicit.
|
||||
|
||||
| Question | Recommended Decision |
|
||||
|---|---|
|
||||
| Are old/historical grades still displayed? | Yes. Always. |
|
||||
| Are old/historical grades recalculated automatically? | No. Never silently. |
|
||||
| Are old blanks converted to missing? | No. Old blanks remain legacy behavior unless explicitly migrated by admin workflow. |
|
||||
| Are scores always out of 100? | No. Add `max_points`, then normalize to percentages for strong scoring. |
|
||||
| What does a blank score mean in the new system? | Nothing final. It means pending or unresolved. |
|
||||
| Does missing work count as zero in the new system? | Yes, unless explicitly excused. |
|
||||
| Does excused work affect the average? | No. Excused work is excluded from the denominator. |
|
||||
| Can a class be locked with pending scores in the new system? | No. |
|
||||
| Can a class be locked with missing work in the new system? | Yes, but missing work counts as zero unless excused. |
|
||||
| Does attendance have one absence grace? | Yes. This is school policy. |
|
||||
| Should final scores keep an audit trail? | Yes. Store a calculation snapshot when locking/finalizing new strong scores. |
|
||||
|
||||
These decisions are non-negotiable if the system needs to be defensible.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Stabilize the Current Legacy System
|
||||
|
||||
Do not start with grading profiles. That is architecture candy. It looks mature, but it does not fix the most dangerous problem first.
|
||||
|
||||
The first goal is to stop bad data, protect current behavior with tests, and keep legacy grade display working.
|
||||
|
||||
### 1. Add Tests for Existing Legacy Formulas
|
||||
|
||||
Before changing calculation behavior, freeze the current formulas in tests.
|
||||
|
||||
Test coverage should include:
|
||||
|
||||
```text
|
||||
homework average ignores blanks
|
||||
quiz average ignores blanks
|
||||
project average ignores blanks
|
||||
participation missing returns null
|
||||
PTAP with no quiz/project
|
||||
PTAP with quiz only
|
||||
PTAP with project only
|
||||
PTAP with all categories
|
||||
Fall semester formula
|
||||
Spring semester formula
|
||||
attendance with 0 absences = 100
|
||||
attendance with 1 absence = 100
|
||||
attendance with 2 absences = reduced score
|
||||
legacy semester_scores display from stored values
|
||||
legacy rows are not recalculated on display
|
||||
```
|
||||
|
||||
This creates a safety net. Without it, every refactor becomes a guessing game with grades, which is exactly as bad as it sounds.
|
||||
|
||||
### 2. Add Central Score Validation
|
||||
|
||||
Create one central validator used by every score write path.
|
||||
|
||||
Validation rules:
|
||||
|
||||
```text
|
||||
score may be null
|
||||
score must be numeric when present
|
||||
score must be >= 0
|
||||
score must be <= max_points
|
||||
max_points must be > 0
|
||||
```
|
||||
|
||||
For the first pass, use `max_points = 100` by default.
|
||||
|
||||
Example service shape:
|
||||
|
||||
```php
|
||||
final class ScoreValueValidator
|
||||
{
|
||||
public function normalizeOrFail(mixed $value, float $maxPoints = 100.0): ?float
|
||||
{
|
||||
if ($value === null || (is_string($value) && trim($value) === '')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!is_numeric($value)) {
|
||||
throw new InvalidArgumentException('Score must be numeric.');
|
||||
}
|
||||
|
||||
$score = (float) $value;
|
||||
|
||||
if ($score < 0 || $score > $maxPoints) {
|
||||
throw new InvalidArgumentException("Score must be between 0 and {$maxPoints}.");
|
||||
}
|
||||
|
||||
return $score;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use this validator in all score-entry paths:
|
||||
|
||||
```text
|
||||
HomeworkScoreService
|
||||
QuizScoreService
|
||||
ProjectScoreService
|
||||
ParticipationScoreService
|
||||
ExamScoreService
|
||||
GradingScoreService
|
||||
```
|
||||
|
||||
This should reject impossible new values without changing how existing historical rows are displayed.
|
||||
|
||||
### 3. Make Attendance Grace Explicit
|
||||
|
||||
Refactor the attendance calculation without changing its result.
|
||||
|
||||
Replace hidden arithmetic:
|
||||
|
||||
```text
|
||||
(total_days + 1 - absences) / total_days
|
||||
```
|
||||
|
||||
With named policy logic:
|
||||
|
||||
```text
|
||||
grace_absences = 1
|
||||
effective_absences = max(0, absences - grace_absences)
|
||||
attendance_score = ((total_days - effective_absences) / total_days) * 100
|
||||
```
|
||||
|
||||
Add tests:
|
||||
|
||||
| Total Days | Absences | Expected Attendance Score |
|
||||
|---:|---:|---:|
|
||||
| 10 | 0 | 100 |
|
||||
| 10 | 1 | 100 |
|
||||
| 10 | 2 | 90 |
|
||||
| 10 | 10 | 10 |
|
||||
| 10 | 11+ | 0 |
|
||||
|
||||
### 4. Add Legacy Display Badge
|
||||
|
||||
Before introducing strong scoring, make the UI able to label old records.
|
||||
|
||||
For legacy rows, show:
|
||||
|
||||
```text
|
||||
Policy: Legacy Calculation
|
||||
```
|
||||
|
||||
Example display:
|
||||
|
||||
```text
|
||||
Semester Score: 91.4
|
||||
Policy: Legacy Calculation
|
||||
Attendance: 100.0
|
||||
PTAP: 88.5
|
||||
Midterm: 92.0
|
||||
|
||||
Note: This score was calculated under the legacy policy. Blank scores were ignored in averages.
|
||||
```
|
||||
|
||||
This prevents users from assuming legacy and strong scores use the same rules.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Make Missing Scores Explicit for New/Strong Scoring
|
||||
|
||||
This phase applies to the new strong scoring path. It must not automatically rewrite old historical records.
|
||||
|
||||
Right now, a blank score can mean too many things:
|
||||
|
||||
```text
|
||||
not graded yet
|
||||
student missing work
|
||||
student excused
|
||||
teacher forgot
|
||||
assignment not applicable
|
||||
```
|
||||
|
||||
A grading system cannot be fair if the data refuses to say what it means.
|
||||
|
||||
### 1. Add Score Statuses
|
||||
|
||||
Every new strong score-bearing record should have both a numeric score and a status.
|
||||
|
||||
Minimum statuses:
|
||||
|
||||
| Status | Meaning | Counts in Average? | Blocks Finalization? |
|
||||
|---|---|---:|---:|
|
||||
| `pending` | Teacher has not resolved the item | No | Yes |
|
||||
| `scored` | Valid numeric score exists | Yes | No |
|
||||
| `missing` | Student did not submit | Yes, as 0 | No |
|
||||
| `excused` | Approved exclusion | No | No |
|
||||
| `not_assigned` | Does not apply | No | No |
|
||||
|
||||
Recommended columns for homework, quiz, project, participation, midterm, and final tables:
|
||||
|
||||
```text
|
||||
status
|
||||
max_points
|
||||
excused_reason
|
||||
locked_at
|
||||
locked_by
|
||||
```
|
||||
|
||||
### 2. Migrate Existing Data Carefully
|
||||
|
||||
Do not blindly turn old blanks into missing work. That would punish students for old system ambiguity.
|
||||
|
||||
Initial migration mapping for active, not-yet-finalized records:
|
||||
|
||||
| Existing Data | Initial Status |
|
||||
|---|---|
|
||||
| Numeric score exists | `scored` |
|
||||
| Blank/null score | `pending` |
|
||||
| Missing override exists and is allowed | `excused` or `not_assigned`, depending policy meaning |
|
||||
|
||||
For historical finalized legacy records, do not require full status migration just to display them. Keep them as legacy display records.
|
||||
|
||||
After migration, teachers/admins must resolve pending items before final lock for strong-scoring sections.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Change Calculation Semantics for Strong Scoring
|
||||
|
||||
Once statuses exist, strong category calculations should stop relying on blank/null behavior.
|
||||
|
||||
New score handling:
|
||||
|
||||
| Status | Calculation Behavior |
|
||||
|---|---|
|
||||
| `scored` | Include normalized score |
|
||||
| `missing` | Include 0 |
|
||||
| `excused` | Exclude from denominator |
|
||||
| `not_assigned` | Exclude from denominator |
|
||||
| `pending` | Block finalization |
|
||||
|
||||
Normalize every scored item:
|
||||
|
||||
```text
|
||||
normalized_score = (raw_score / max_points) * 100
|
||||
```
|
||||
|
||||
Then calculate the category average:
|
||||
|
||||
```text
|
||||
category_average = average(all included normalized scores)
|
||||
```
|
||||
|
||||
This prevents missing work from silently improving a student's grade.
|
||||
|
||||
Legacy records must still display legacy stored values. Do not apply this status-aware calculation to historical rows unless an admin explicitly opts into a recalculation workflow.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Add Finalization Validation
|
||||
|
||||
Locking a strong-scoring class should not simply flip a flag. It should prove the gradebook is complete enough to finalize.
|
||||
|
||||
Create a validator service:
|
||||
|
||||
```php
|
||||
App\Services\Grading\GradebookFinalizationValidator
|
||||
```
|
||||
|
||||
It should validate:
|
||||
|
||||
```text
|
||||
No pending required scores
|
||||
All numeric scores are within range
|
||||
Required categories are present
|
||||
Required exam score exists
|
||||
Required participation score exists
|
||||
Attendance can be calculated
|
||||
Missing items are either counted as zero or excused
|
||||
Every student has a calculable final score
|
||||
```
|
||||
|
||||
If validation fails, return a structured report:
|
||||
|
||||
```text
|
||||
student
|
||||
category
|
||||
item
|
||||
problem
|
||||
required_action
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
Student: Ahmed Ali
|
||||
Category: Homework
|
||||
Item: Homework #4
|
||||
Problem: Pending score
|
||||
Required action: Mark as scored, missing, excused, or not assigned before locking.
|
||||
```
|
||||
|
||||
The lock button should become a gate, not a decorative suggestion.
|
||||
|
||||
Legacy historical records do not need to pass the new finalization validator just to be displayed.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Add Calculation Snapshots for Strong Scores
|
||||
|
||||
When strong grades are finalized or locked, store exactly how the final score was produced.
|
||||
|
||||
Create table:
|
||||
|
||||
```text
|
||||
semester_score_snapshots
|
||||
```
|
||||
|
||||
Recommended fields:
|
||||
|
||||
```text
|
||||
id
|
||||
student_id
|
||||
class_section_id
|
||||
semester
|
||||
school_year
|
||||
grading_policy_version
|
||||
input_json
|
||||
calculation_json
|
||||
semester_score
|
||||
calculated_by
|
||||
calculated_at
|
||||
```
|
||||
|
||||
The snapshot should include:
|
||||
|
||||
```text
|
||||
raw scores used
|
||||
score statuses used
|
||||
max points used
|
||||
normalized scores
|
||||
category averages
|
||||
category weights
|
||||
attendance grace policy
|
||||
exam score
|
||||
final formula
|
||||
final score
|
||||
```
|
||||
|
||||
This protects the school when someone disputes a grade later.
|
||||
|
||||
The current `semester_scores` table can show the latest aggregate. The snapshot proves what was true at finalization.
|
||||
|
||||
Legacy historical grades may have no snapshot. That is acceptable as long as they are clearly labeled `legacy_v1` and displayed from stored values.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Replace Hardcoded PTAP With Grading Profiles
|
||||
|
||||
Only after validation, statuses, legacy display protection, and snapshots are in place should hardcoded weighting be replaced.
|
||||
|
||||
Current PTAP behavior changes based on data availability:
|
||||
|
||||
```text
|
||||
No tests and no projects
|
||||
Tests but no projects
|
||||
Projects but no tests
|
||||
All categories exist
|
||||
```
|
||||
|
||||
That is flexible, but dangerous. It lets missing or unentered data affect the formula.
|
||||
|
||||
### Recommended Tables
|
||||
|
||||
```text
|
||||
grading_profiles
|
||||
```
|
||||
|
||||
Fields:
|
||||
|
||||
```text
|
||||
id
|
||||
name
|
||||
school_year
|
||||
semester
|
||||
class_section_id nullable
|
||||
is_default
|
||||
version
|
||||
created_by
|
||||
created_at
|
||||
updated_at
|
||||
```
|
||||
|
||||
```text
|
||||
grading_profile_categories
|
||||
```
|
||||
|
||||
Fields:
|
||||
|
||||
```text
|
||||
id
|
||||
grading_profile_id
|
||||
category_key
|
||||
weight
|
||||
required
|
||||
redistribute_if_missing
|
||||
```
|
||||
|
||||
### Example Profile
|
||||
|
||||
| Category | Weight | Required | Redistribute If Unused |
|
||||
|---|---:|---:|---:|
|
||||
| homework | 15 | Yes | No |
|
||||
| quiz | 15 | Configurable | Yes |
|
||||
| project | 15 | Configurable | Yes |
|
||||
| participation | 15 | Yes | No |
|
||||
| attendance | 10 | Yes | No |
|
||||
| exam | 30 | Yes | No |
|
||||
|
||||
Important distinction:
|
||||
|
||||
```text
|
||||
Category not used by profile -> redistribute its weight
|
||||
Category assigned but scores pending -> block lock
|
||||
Category assigned but student missing -> count as zero
|
||||
Category excused -> exclude according to policy
|
||||
```
|
||||
|
||||
Do not redistribute weight just because a teacher forgot to enter grades. That would be grading by accident, which is somehow worse than grading by spreadsheet.
|
||||
|
||||
Legacy rows continue using legacy PTAP display and must not be forced into grading profiles.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Opt-In Activation Strategy
|
||||
|
||||
Strong scoring should be activated by class section, semester, and school year. It should not globally replace legacy behavior in one switch.
|
||||
|
||||
Recommended activation metadata:
|
||||
|
||||
```text
|
||||
class_section_id
|
||||
school_year
|
||||
semester
|
||||
calculation_mode = legacy | strong
|
||||
grading_profile_id nullable
|
||||
activated_by
|
||||
activated_at
|
||||
```
|
||||
|
||||
Activation rules:
|
||||
|
||||
1. Existing historical records remain `legacy`.
|
||||
2. Current active sections may remain `legacy` until admins choose migration.
|
||||
3. New future sections can default to `strong` once the system is ready.
|
||||
4. A class section using `legacy` must continue calculating and displaying using the legacy service.
|
||||
5. A class section using `strong` must use statuses, max points, finalization validation, and snapshots.
|
||||
6. Switching a section from legacy to strong should require an admin confirmation and a preflight report.
|
||||
|
||||
Preflight report should show:
|
||||
|
||||
```text
|
||||
number of numeric scores that will become scored
|
||||
number of blanks that will become pending
|
||||
number of overrides requiring review
|
||||
students blocked from finalization until pending items are resolved
|
||||
```
|
||||
|
||||
No quiet migrations. Quiet migrations are where data integrity goes to disappear.
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Refactor Controllers Last
|
||||
|
||||
Controllers should not contain grading policy.
|
||||
|
||||
Final desired flow:
|
||||
|
||||
```text
|
||||
Controller receives request
|
||||
Request validates input shape
|
||||
Application service saves score/status
|
||||
Scoring mode resolver selects legacy or strong path
|
||||
Scoring service recalculates draft score
|
||||
Finalization validator blocks or allows lock for strong mode
|
||||
Snapshot service records locked strong calculation
|
||||
Display resolver shows legacy or strong breakdown
|
||||
```
|
||||
|
||||
Controllers should not know formulas. They should not decide whether missing counts. They should not know category redistribution rules.
|
||||
|
||||
Controllers are traffic cops, not education philosophers.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Implementation Order
|
||||
|
||||
| Step | Work | Risk Reduced |
|
||||
|---:|---|---|
|
||||
| 1 | Add tests for current legacy formulas and legacy display | Prevent accidental formula/display drift |
|
||||
| 2 | Add compatibility metadata to `semester_scores` | Preserve old records clearly |
|
||||
| 3 | Add legacy/strong display resolver | Keep old data visible |
|
||||
| 4 | Add central score validation | Stop impossible new scores |
|
||||
| 5 | Make attendance grace explicit | Preserve policy and improve clarity |
|
||||
| 6 | Add `max_points` columns | Enable normalization |
|
||||
| 7 | Add `status` columns | Remove blank-score ambiguity for strong scoring |
|
||||
| 8 | Build category calculator using statuses | Fix missing-score fairness |
|
||||
| 9 | Add finalization validator | Stop invalid strong locks |
|
||||
| 10 | Add calculation snapshots | Audit-proof locked strong grades |
|
||||
| 11 | Add grading profiles | Remove hardcoded weights for strong mode |
|
||||
| 12 | Add opt-in activation by class/semester | Prevent disruptive rollout |
|
||||
| 13 | Refactor controllers | Clean architecture after behavior is safe |
|
||||
|
||||
---
|
||||
|
||||
## MVP Strong Upgrade
|
||||
|
||||
The smallest serious upgrade is:
|
||||
|
||||
```text
|
||||
1. Keep current legacy formulas.
|
||||
2. Keep old data display from stored semester_scores values.
|
||||
3. Add calculation_mode and calculation_policy_version metadata.
|
||||
4. Add legacy display badge.
|
||||
5. Add central score validation for new writes.
|
||||
6. Make attendance grace explicit without changing results.
|
||||
7. Add score statuses for active/future strong-scoring sections.
|
||||
8. Treat missing as zero, excused as excluded, and pending as blocking lock in strong mode only.
|
||||
9. Add a lock validation report for strong mode.
|
||||
10. Add calculation snapshots on strong-mode lock.
|
||||
```
|
||||
|
||||
This improves fairness and auditability without breaking old records or immediately changing all weights.
|
||||
|
||||
After that, replace PTAP branching with grading profiles.
|
||||
|
||||
---
|
||||
|
||||
## Hard Safety Rules
|
||||
|
||||
1. Do not change formulas and database semantics in the same step without tests.
|
||||
2. Do not silently recalculate historical scores.
|
||||
3. Do not convert old blanks to missing for historical records.
|
||||
4. Do not hide legacy data because it lacks new statuses.
|
||||
5. Do not activate strong scoring globally without class/semester-level opt-in.
|
||||
6. Do not display a strong-policy explanation for a legacy-policy score.
|
||||
7. Do not let a lock action become the first time the system discovers incomplete data.
|
||||
|
||||
That is how a school ends up unable to explain whether grades changed because of policy, migration, code, or a null-handling goblin hiding in the service layer.
|
||||
|
||||
---
|
||||
|
||||
## Final Recommendation
|
||||
|
||||
Do not begin by building grading profiles.
|
||||
|
||||
Begin by making legacy display safe and every new score state explicit.
|
||||
|
||||
The strongest first milestone is:
|
||||
|
||||
```text
|
||||
Old grades remain visible under Legacy Calculation, and future strong-mode grades cannot be locked until every score is classified as scored, missing, excused, pending, or not assigned.
|
||||
```
|
||||
|
||||
Once that is true, the grading system becomes defensible. Until that is true, every formula is built on fog.
|
||||
Reference in New Issue
Block a user