Skip to content

bug: /api/skill-progression/record bypasses prerequisites and allows silent difficulty downgrades (regression of #814) #1869

Description

@ionfwsrijan

Summary

POST /api/skill-progression/record never validates prerequisites and never enforces difficulty monotonicity. A client can claim Advanced for React without any JavaScript baseline in a single request, or silently downgrade an existing Expert skill back to Beginner. The validation endpoint (/validate) exists but record does not call it — so #814 ("Skill difficulty progression not validated") remains unfixed in practice.

Evidence

src/routes/main_routes.py:578-626 (record_skill_completion route) validates only:

  • that difficulty maps to a known SkillDifficulty enum name (SkillDifficulty[difficulty.upper()], line 597), and
  • that assessment_score is a number in 0-100 (lines 601-611).

It never calls can_learn_skill / validate_skill_progression before recording.

src/utils/skill_progression.py:160-207 (record_skill_completion):

if skill_name not in self.user_skills[user_id]:
    self.user_skills[user_id][skill_name] = {"difficulty": None, ...}
skill_data = self.user_skills[user_id][skill_name]
skill_data["difficulty"] = difficulty          # line 196 — unconditional overwrite, no monotonic check

SKILL_PREREQUISITES (lines 41-112) defines real dependency rules (e.g. React ADVANCED requires JavaScript ADVANCED), and can_learn_skill (lines 121-158) enforces them — but nothing in the write path consults it. The get_recommended_next_skill helper (lines 213-241) even computes the next step from current_difficulty.value + 1, implying levels should move one at a time.

Impact

  • Users can claim the highest tier of any skill instantly, bypassing the entire prerequisite graph.
  • Re-recording a skill at a lower difficulty silently destroys the higher stored level (no max() / monotonic check), so progress can be trivially wiped or corrupted.

Suggested Fix

Before storing, route record through can_learn_skill(user_id, skill_name, difficulty) (return 400 with the message when not allowed), and preserve the higher existing difficulty when a lower one is submitted (e.g. skill_data["difficulty"] = max(skill_data["difficulty"] or difficulty, difficulty)). Add route-level tests for prereq violation and downgrade.

Activity

  1. github-actions commented on Oct 9, 2026

    @github-actions

    This issue has been automatically marked as stale because it has not had recent activity. It will be closed in 7 days if no further activity occurs. Thank you for your contributions!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions