Skip to content

feat: automatic plagiarism detection (Issue #45) - #47

Open
Sonchik888 wants to merge 2 commits into
markpolyak:mainfrom
Sonchik888:feature/plagiarism-detection
Open

feat: automatic plagiarism detection (Issue #45)#47
Sonchik888 wants to merge 2 commits into
markpolyak:mainfrom
Sonchik888:feature/plagiarism-detection

Conversation

@Sonchik888

@Sonchik888 Sonchik888 commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Local plagiarism checks with compare50 after a successful v grade (FastAPI BackgroundTasks)
  • Source cache, SQLite matches, Google Sheets cell notes, admin UI + review flag
  • Course config plagiarism: with deprecated moss: alias; docs, tests, CLI scripts/menu.py
  • Phase 0 research: compare50 vs JPlag; cell-anchored Drive comments unavailable → notes only

Closes #45

Plan phases (docs/PLAGIARISM_DETECTION_PLAN.md)

  • 0 Research (compare50 vs JPlag, Sheets notes visibility)
  • 1 Source cache
  • 2 Engine wrapper (compare50)
  • 3 Grading integration + SQLite
  • 4 Teacher reporting (notes + admin UI)
  • 5 Config + documentation
  • 6 Tests + pilot (PLAGIARISM_SHADOW_MODE)

Test plan

  • pytest tests/test_plagiarism*.py tests/test_sheets_comments.py -v
  • python scripts/menu.py → 8 → admin login → course → lab → plagiarism list → Mark reviewed
  • Grade a student with PLAGIARISM_SHADOW_MODE=false → note appears on Sheets cell
  • Shadow mode stores DB matches without notes

Made with Cursor

Sonchik888 and others added 2 commits July 24, 2026 13:52
Implement local compare50 checks after writing v to Sheets, with source cache, SQLite matches, cell notes, and admin UI per docs/PLAGIARISM_DETECTION_PLAN.md (closes issue markpolyak#45 phases 0-6).

Co-authored-by: Cursor <cursoragent@cursor.com>
grade_lab now schedules plagiarism checks via BackgroundTasks; update calls so CI characterization tests match the new signature.

Co-authored-by: Cursor <cursoragent@cursor.com>

@markpolyak markpolyak left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Ревью (multi-angle, high effort)

PR большой (5206 добавленных строк, 37 файлов), реализует локальную проверку на плагиат через compare50 после успешной оценки. Ниже — находки, отсортированные по серьёзности, прошедшие независимую верификацию.

Баги (влияют на корректность/данные)

  1. Файлы-шаблоны (basefiles) попадают в сравнение как «поддельная» студенческая работаgrading/plagiarism_cache.py (list_cached_submissions, ~L224). Функция обходит все поддиректории lab_root как «организации», не исключая служебную папку _basefiles, куда _ensure_basefiles кладёт код шаблона. В результате шаблонный код сравнивается через compare50 наравне со студентами → ложные обвинения в плагиате для тех, кто честно использовал шаблон как есть.

  2. Заметка о плагиате может уйти не в ту ячейкуmain.py (~L936) передаёт cell_row/cell_col по значению в BackgroundTasks при постановке задачи, а сама проверка (скачивание репозиториев + compare50) может выполняться секунды-минуты. Если за это время студент пересдаёт работу и позиция в таблице меняется, заметка о плагиате уйдёт в устаревшую/чужую ячейку — нет повторной проверки актуальности перед записью.

  3. Блокировки SQLite тихо проглатываютсяgrading/plagiarism_store.py (~L30): sqlite3.connect() без timeout=/WAL, а run_plagiarism_check оборачивает всё в широкий except Exception: logger.exception(...). При параллельных фоновых проверках возможна ошибка database is locked, которая молча проглатывается — совпадения для студента просто не сохраняются, без какого-либо сигнала.

  4. URL к GitHub Contents API не экранируетсяgrading/github_client.py (get_file_content, ~L96). Путь к файлу подставляется в f-string без URL-кодирования; имя файла с пробелом/спецсимволом ломает запрос, GitHub отвечает ошибкой, и файл молча пропускается из кэша (только debug-лог) — часть кода студента вообще не участвует в проверке.

  5. Конфиг plagiarism.language — нерабочая опция — поле PlagiarismConfig.language парсится из YAML, но нигде не используется в реальном пайплайне (run_plagiarism_check/run_compare50 его не читают). Преподаватель может выставить language: cpp и не получить ни ошибки, ни эффекта — тихий no-op.

  6. Заметка на ячейке перезаписывается, а не дополняетсяgrading/sheets_comments.py (set_cell_note, ~L84). Каждый новый прогон полностью заменяет содержимое заметки текущими совпадениями, теряя информацию о более ранних находках.

  7. Потенциальный path traversal через конфиг files: (правдоподобно, не 100% доказано) — grading/plagiarism_cache.py (~L178): имя файла берётся напрямую из YAML-конфига лабы и join'ится в путь кэша без санитизации (../абсолютные пути могут вывести запись за пределы plagiarism_cache/...).

  8. Monkey-patch глобального состояния compare50 без блокировкиgrading/plagiarism.py (run_compare50, ~L178) патчит compare50._data.File.read/compare50._api.Executor на уровне модуля. BackgroundTasks в Starlette реально исполняются в threadpool, так что при двух параллельных фоновых проверках это гонка (сейчас безобидная, т.к. оба потока пишут одинаковые значения, но заложена бомба замедленного действия).

Дублирование кода / архитектура

  1. _ensure_basefiles продублирован и уже разошёлся между grading/plagiarism_check.py (L46-85) и scripts/plagiarism_batch_course.py (L76-106) — разная защита от некорректного repo_full, разное логирование. Стоит вынести в общий helper.

  2. Аутентификация Google Sheets задублированаgrading/sheets_comments.py реализует собственный ServiceAccountCredentials/gspread.authorize независимо от уже открытой в main.py сессии. Каждая проверка на плагиат — лишний полный auth-round-trip к Sheets API вместо переиспользования уже открытого листа.

Проверено и опровергнуто

  • Дефолт max-matches 250→50 — не регрессия, фича полностью новая, старого дефолта не существовало.
  • Смена дефолта CREDENTIALS_FILE в .env.example — не проблема, docker-compose.example.yaml явно переопределяет путь для Docker.
  • Версионная ветка hasattr(sheet, 'update_note') — на gspread 6.2.1 оба метода эквивалентны, первая заметка не ломается.
  • Проверка cell_row and cell_col на truthy — 0 недостижим на практике (индексы всегда ≥3/≥1).

Рекомендация

Самое важное до мержа — пункты 1 и 2 (ложные совпадения с шаблоном и запись заметки не в ту ячейку): оба напрямую бьют по доверию к системе. Остальное можно чинить отдельными PR после мержа.


🤖 Автоматическое multi-angle ревью (8 угла поиска + верификация), выполненное Claude Code.

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