feat(quiz): server-authoritative grading — per-question answers, quiz_responses, answer-key deprecation (#541) - #549

Merged
AndresL230 merged 2 commits into
mainfrom
feat/541-quiz-answer-endpoint
Aug 13, 2026
Merged

feat(quiz): server-authoritative grading — per-question answers, quiz_responses, answer-key deprecation (#541)#549
AndresL230 merged 2 commits into
mainfrom
feat/541-quiz-answer-endpoint

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

Closes#541. Workstream C of the pre-revamp quiz repair batch (epic #537). Today the answer key ships to the browser and review grades locally; every interesting version of the #537 flow (in-session adaptivity, confidence, timing, honest results) needs the server to be the one that knows. This PR gives the new client its grading API while the current client keeps working unchanged.

What

C1 — POST /api/quiz/attempts/{attempt_id}/answer.{question_index, selected_index, time_ms?, confidence?}{is_correct, correct_index, explanation, next_question, recorded}. Owner check; 409 QUIZ_ATTEMPT_ALREADY_COMPLETED after completion; 400 QUIZ_QUESTION_INVALID on out-of-range indexes; malformed items (no correct option) never grade correct (#129 rule); next_question comes back stripped of the answer key. Idempotent on (attempt_id, question_index): re-answering returns the FIRST recorded response with recorded: false — no revision, decided and documented; a lost insert race re-reads whatever the UNIQUE let win.

C2 — quiz_responses (migration 20260812214402): attempt_id (FK cascade), question_index, selected_index, is_correct, time_ms, confidence, answered_at. All plaintext analytics scalars (#521 rationale — no free text; question/option text stays encrypted in questions_json). UNIQUE (attempt_id, question_index) is the idempotency contract; real-DB integration tests pin the UNIQUE arbitration and the cascade.

C3 — include_answer_key on generate, default true so the current QuizPanel keeps working. Every keyed response logs a deprecation breadcrumb; false strips per-option correct booleans from the response while storage keeps them for server-side grading. Flip + delete tracked in #546; removal is a hard requirement of #537.

C4 — submit reconciles. Responses recorded through C1 are the source of truth per question (a contradicting payload answer is ignored — letting the final POST override answer-time grades would reopen the client-grading hole); questions never answered through C1 fall back to the payload, so the current all-at-the-end client is unaffected. The atomic completed_at claim (PR #464) and 409 behaviour are untouched. Mixed-case and recorded-only tests included.

Wire-contract notes (current client)

  • Generate: include_answer_key is additive with a compatible default; response shape unchanged for the old client.
  • Submit: behaviour identical when no C1 responses exist (the old client never creates any).
  • New endpoint is additive.

Verification

  • Hermetic suite: 1938 passed (10 new tests, written first, watched fail).
  • Full local cycle under the stack lock: Playwright 43 → oracles exit 0 → integration lane (counts checked) → teardown.
  • ruff check clean. Migration replayed from empty by the stack bring-up; applied to staging ahead of merge.

🤖 Generated with Claude Code

…_responses, answer-key deprecation (#541)
Workstream C of the pre-revamp quiz repair batch (epic #537):
C1 — POST /api/quiz/attempts/{attempt_id}/answer grades one question
server-side: owner check, 409 after completion, 400 QUIZ_QUESTION_INVALID
on out-of-range indexes, malformed items never grade correct (#129 rule).
Idempotent on (attempt_id, question_index): re-answering returns the
FIRST recorded response with recorded:false — no revision, decided and
documented for the #537 flow. Returns is_correct/correct_index/
explanation plus the next question stripped of the answer key.
C2 — quiz_responses table (migration 20260812214402): plaintext
analytics scalars only (indexes, boolean, time_ms, confidence), UNIQUE
(attempt_id, question_index) as the idempotency contract, FK cascade
with the attempt. Real-DB integration tests pin the UNIQUE arbitration
and the cascade.
C3 — include_answer_key on generate, default true so the current
QuizPanel keeps working; every keyed response logs a deprecation
breadcrumb; false strips per-option correct booleans from the response
while storage keeps them for grading. Removal tracked in #546; deleting
the key is a hard requirement of #537.
C4 — submit prefers recorded quiz_responses per question (a
contradicting payload answer is ignored — answer-time grades are the
source of truth) and falls back to the payload for questions never
answered through C1. The atomic completed_at claim (PR #464) and 409
behaviour are untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@supabase

supabaseBot commented Aug 13, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 13, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingc2d316eCommit Preview URL

Branch Preview URL
Aug 13 2026, 02:09 AM

@coderabbitai

coderabbitaiBot commented Aug 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:57 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3676f4bd-bdde-47dd-903a-187dd2195fe4

📥 Commits

Reviewing files that changed from the base of the PR and between 5e1a322 and c2d316e.

📒 Files selected for processing (6)
  • backend/db/migrations/20260812214402_quiz_responses.sql
  • backend/models/__init__.py
  • backend/routes/quiz.py
  • backend/services/quiz_errors.py
  • backend/tests/integration/test_quiz_responses_db.py
  • backend/tests/test_quiz_answers_c.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…e index/id mismatch, drop the redundant index
Review findings (xhigh, 4 confirmed; 5 reports collapsed to one root cause):
- submit now persists the RECONCILED answer set (recorded responses
winning over payload) instead of the raw request body. A recorded-only
submit previously stored a full score beside answers_json=[], and a
contradicted payload answer was stored despite losing to the recorded
response — the attempt record disagreed with its own score.
- The answer endpoint accepts an optional question_id and rejects a
mismatch with question_index, plus echoes both in the response: passing
the 1-based wire id as the 0-based index used to silently grade the
neighbouring question, which idempotency then locked in.
- quiz_responses drops the standalone attempt_id index — the UNIQUE's
btree already leads with attempt_id, so it only added a write to the
per-answer hot path.
- correct_index is resolved once per request instead of re-scanning the
options on every _graded call (it never depended on the answer).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 73fb504 into mainAug 13, 2026
4 of 7 checks passed
Sign up for freeto 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.

quiz C: server-authoritative grading — per-question answer endpoint, quiz_responses table, answer-key deprecation

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(quiz): server-authoritative grading — per-question answers, quiz_responses, answer-key deprecation (#541) - #549

Merged
AndresL230 merged 2 commits into
mainfrom
feat/541-quiz-answer-endpoint
Aug 13, 2026
Merged

feat(quiz): server-authoritative grading — per-question answers, quiz_responses, answer-key deprecation (#541)#549
AndresL230 merged 2 commits into
mainfrom
feat/541-quiz-answer-endpoint

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

Closes#541. Workstream C of the pre-revamp quiz repair batch (epic #537). Today the answer key ships to the browser and review grades locally; every interesting version of the #537 flow (in-session adaptivity, confidence, timing, honest results) needs the server to be the one that knows. This PR gives the new client its grading API while the current client keeps working unchanged.

What

C1 — POST /api/quiz/attempts/{attempt_id}/answer.{question_index, selected_index, time_ms?, confidence?}{is_correct, correct_index, explanation, next_question, recorded}. Owner check; 409 QUIZ_ATTEMPT_ALREADY_COMPLETED after completion; 400 QUIZ_QUESTION_INVALID on out-of-range indexes; malformed items (no correct option) never grade correct (#129 rule); next_question comes back stripped of the answer key. Idempotent on (attempt_id, question_index): re-answering returns the FIRST recorded response with recorded: false — no revision, decided and documented; a lost insert race re-reads whatever the UNIQUE let win.

C2 — quiz_responses (migration 20260812214402): attempt_id (FK cascade), question_index, selected_index, is_correct, time_ms, confidence, answered_at. All plaintext analytics scalars (#521 rationale — no free text; question/option text stays encrypted in questions_json). UNIQUE (attempt_id, question_index) is the idempotency contract; real-DB integration tests pin the UNIQUE arbitration and the cascade.

C3 — include_answer_key on generate, default true so the current QuizPanel keeps working. Every keyed response logs a deprecation breadcrumb; false strips per-option correct booleans from the response while storage keeps them for server-side grading. Flip + delete tracked in #546; removal is a hard requirement of #537.

C4 — submit reconciles. Responses recorded through C1 are the source of truth per question (a contradicting payload answer is ignored — letting the final POST override answer-time grades would reopen the client-grading hole); questions never answered through C1 fall back to the payload, so the current all-at-the-end client is unaffected. The atomic completed_at claim (PR #464) and 409 behaviour are untouched. Mixed-case and recorded-only tests included.

Wire-contract notes (current client)

  • Generate: include_answer_key is additive with a compatible default; response shape unchanged for the old client.
  • Submit: behaviour identical when no C1 responses exist (the old client never creates any).
  • New endpoint is additive.

Verification

  • Hermetic suite: 1938 passed (10 new tests, written first, watched fail).
  • Full local cycle under the stack lock: Playwright 43 → oracles exit 0 → integration lane (counts checked) → teardown.
  • ruff check clean. Migration replayed from empty by the stack bring-up; applied to staging ahead of merge.

🤖 Generated with Claude Code

…_responses, answer-key deprecation (#541)
Workstream C of the pre-revamp quiz repair batch (epic #537):
C1 — POST /api/quiz/attempts/{attempt_id}/answer grades one question
server-side: owner check, 409 after completion, 400 QUIZ_QUESTION_INVALID
on out-of-range indexes, malformed items never grade correct (#129 rule).
Idempotent on (attempt_id, question_index): re-answering returns the
FIRST recorded response with recorded:false — no revision, decided and
documented for the #537 flow. Returns is_correct/correct_index/
explanation plus the next question stripped of the answer key.
C2 — quiz_responses table (migration 20260812214402): plaintext
analytics scalars only (indexes, boolean, time_ms, confidence), UNIQUE
(attempt_id, question_index) as the idempotency contract, FK cascade
with the attempt. Real-DB integration tests pin the UNIQUE arbitration
and the cascade.
C3 — include_answer_key on generate, default true so the current
QuizPanel keeps working; every keyed response logs a deprecation
breadcrumb; false strips per-option correct booleans from the response
while storage keeps them for grading. Removal tracked in #546; deleting
the key is a hard requirement of #537.
C4 — submit prefers recorded quiz_responses per question (a
contradicting payload answer is ignored — answer-time grades are the
source of truth) and falls back to the payload for questions never
answered through C1. The atomic completed_at claim (PR #464) and 409
behaviour are untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@supabase

supabaseBot commented Aug 13, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 13, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingc2d316eCommit Preview URL

Branch Preview URL
Aug 13 2026, 02:09 AM

@coderabbitai

coderabbitaiBot commented Aug 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:57 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3676f4bd-bdde-47dd-903a-187dd2195fe4

📥 Commits

Reviewing files that changed from the base of the PR and between 5e1a322 and c2d316e.

📒 Files selected for processing (6)
  • backend/db/migrations/20260812214402_quiz_responses.sql
  • backend/models/__init__.py
  • backend/routes/quiz.py
  • backend/services/quiz_errors.py
  • backend/tests/integration/test_quiz_responses_db.py
  • backend/tests/test_quiz_answers_c.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…e index/id mismatch, drop the redundant index
Review findings (xhigh, 4 confirmed; 5 reports collapsed to one root cause):
- submit now persists the RECONCILED answer set (recorded responses
winning over payload) instead of the raw request body. A recorded-only
submit previously stored a full score beside answers_json=[], and a
contradicted payload answer was stored despite losing to the recorded
response — the attempt record disagreed with its own score.
- The answer endpoint accepts an optional question_id and rejects a
mismatch with question_index, plus echoes both in the response: passing
the 1-based wire id as the 0-based index used to silently grade the
neighbouring question, which idempotency then locked in.
- quiz_responses drops the standalone attempt_id index — the UNIQUE's
btree already leads with attempt_id, so it only added a write to the
per-answer hot path.
- correct_index is resolved once per request instead of re-scanning the
options on every _graded call (it never depended on the answer).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 73fb504 into mainAug 13, 2026
4 of 7 checks passed
Sign up for freeto 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.

quiz C: server-authoritative grading — per-question answer endpoint, quiz_responses table, answer-key deprecation

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(quiz): server-authoritative grading — per-question answers, quiz_responses, answer-key deprecation (#541) - #549

Merged
AndresL230 merged 2 commits into
mainfrom
feat/541-quiz-answer-endpoint
Aug 13, 2026
Merged

feat(quiz): server-authoritative grading — per-question answers, quiz_responses, answer-key deprecation (#541)#549
AndresL230 merged 2 commits into
mainfrom
feat/541-quiz-answer-endpoint

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

Closes#541. Workstream C of the pre-revamp quiz repair batch (epic #537). Today the answer key ships to the browser and review grades locally; every interesting version of the #537 flow (in-session adaptivity, confidence, timing, honest results) needs the server to be the one that knows. This PR gives the new client its grading API while the current client keeps working unchanged.

What

C1 — POST /api/quiz/attempts/{attempt_id}/answer.{question_index, selected_index, time_ms?, confidence?}{is_correct, correct_index, explanation, next_question, recorded}. Owner check; 409 QUIZ_ATTEMPT_ALREADY_COMPLETED after completion; 400 QUIZ_QUESTION_INVALID on out-of-range indexes; malformed items (no correct option) never grade correct (#129 rule); next_question comes back stripped of the answer key. Idempotent on (attempt_id, question_index): re-answering returns the FIRST recorded response with recorded: false — no revision, decided and documented; a lost insert race re-reads whatever the UNIQUE let win.

C2 — quiz_responses (migration 20260812214402): attempt_id (FK cascade), question_index, selected_index, is_correct, time_ms, confidence, answered_at. All plaintext analytics scalars (#521 rationale — no free text; question/option text stays encrypted in questions_json). UNIQUE (attempt_id, question_index) is the idempotency contract; real-DB integration tests pin the UNIQUE arbitration and the cascade.

C3 — include_answer_key on generate, default true so the current QuizPanel keeps working. Every keyed response logs a deprecation breadcrumb; false strips per-option correct booleans from the response while storage keeps them for server-side grading. Flip + delete tracked in #546; removal is a hard requirement of #537.

C4 — submit reconciles. Responses recorded through C1 are the source of truth per question (a contradicting payload answer is ignored — letting the final POST override answer-time grades would reopen the client-grading hole); questions never answered through C1 fall back to the payload, so the current all-at-the-end client is unaffected. The atomic completed_at claim (PR #464) and 409 behaviour are untouched. Mixed-case and recorded-only tests included.

Wire-contract notes (current client)

  • Generate: include_answer_key is additive with a compatible default; response shape unchanged for the old client.
  • Submit: behaviour identical when no C1 responses exist (the old client never creates any).
  • New endpoint is additive.

Verification

  • Hermetic suite: 1938 passed (10 new tests, written first, watched fail).
  • Full local cycle under the stack lock: Playwright 43 → oracles exit 0 → integration lane (counts checked) → teardown.
  • ruff check clean. Migration replayed from empty by the stack bring-up; applied to staging ahead of merge.

🤖 Generated with Claude Code

…_responses, answer-key deprecation (#541)
Workstream C of the pre-revamp quiz repair batch (epic #537):
C1 — POST /api/quiz/attempts/{attempt_id}/answer grades one question
server-side: owner check, 409 after completion, 400 QUIZ_QUESTION_INVALID
on out-of-range indexes, malformed items never grade correct (#129 rule).
Idempotent on (attempt_id, question_index): re-answering returns the
FIRST recorded response with recorded:false — no revision, decided and
documented for the #537 flow. Returns is_correct/correct_index/
explanation plus the next question stripped of the answer key.
C2 — quiz_responses table (migration 20260812214402): plaintext
analytics scalars only (indexes, boolean, time_ms, confidence), UNIQUE
(attempt_id, question_index) as the idempotency contract, FK cascade
with the attempt. Real-DB integration tests pin the UNIQUE arbitration
and the cascade.
C3 — include_answer_key on generate, default true so the current
QuizPanel keeps working; every keyed response logs a deprecation
breadcrumb; false strips per-option correct booleans from the response
while storage keeps them for grading. Removal tracked in #546; deleting
the key is a hard requirement of #537.
C4 — submit prefers recorded quiz_responses per question (a
contradicting payload answer is ignored — answer-time grades are the
source of truth) and falls back to the payload for questions never
answered through C1. The atomic completed_at claim (PR #464) and 409
behaviour are untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@supabase

supabaseBot commented Aug 13, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 13, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingc2d316eCommit Preview URL

Branch Preview URL
Aug 13 2026, 02:09 AM

@coderabbitai

coderabbitaiBot commented Aug 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:57 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3676f4bd-bdde-47dd-903a-187dd2195fe4

📥 Commits

Reviewing files that changed from the base of the PR and between 5e1a322 and c2d316e.

📒 Files selected for processing (6)
  • backend/db/migrations/20260812214402_quiz_responses.sql
  • backend/models/__init__.py
  • backend/routes/quiz.py
  • backend/services/quiz_errors.py
  • backend/tests/integration/test_quiz_responses_db.py
  • backend/tests/test_quiz_answers_c.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…e index/id mismatch, drop the redundant index
Review findings (xhigh, 4 confirmed; 5 reports collapsed to one root cause):
- submit now persists the RECONCILED answer set (recorded responses
winning over payload) instead of the raw request body. A recorded-only
submit previously stored a full score beside answers_json=[], and a
contradicted payload answer was stored despite losing to the recorded
response — the attempt record disagreed with its own score.
- The answer endpoint accepts an optional question_id and rejects a
mismatch with question_index, plus echoes both in the response: passing
the 1-based wire id as the 0-based index used to silently grade the
neighbouring question, which idempotency then locked in.
- quiz_responses drops the standalone attempt_id index — the UNIQUE's
btree already leads with attempt_id, so it only added a write to the
per-answer hot path.
- correct_index is resolved once per request instead of re-scanning the
options on every _graded call (it never depended on the answer).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 73fb504 into mainAug 13, 2026
4 of 7 checks passed
Sign up for freeto 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.

quiz C: server-authoritative grading — per-question answer endpoint, quiz_responses table, answer-key deprecation

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(quiz): server-authoritative grading — per-question answers, quiz_responses, answer-key deprecation (#541) - #549

Merged
AndresL230 merged 2 commits into
mainfrom
feat/541-quiz-answer-endpoint
Aug 13, 2026
Merged

feat(quiz): server-authoritative grading — per-question answers, quiz_responses, answer-key deprecation (#541)#549
AndresL230 merged 2 commits into
mainfrom
feat/541-quiz-answer-endpoint

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

Closes#541. Workstream C of the pre-revamp quiz repair batch (epic #537). Today the answer key ships to the browser and review grades locally; every interesting version of the #537 flow (in-session adaptivity, confidence, timing, honest results) needs the server to be the one that knows. This PR gives the new client its grading API while the current client keeps working unchanged.

What

C1 — POST /api/quiz/attempts/{attempt_id}/answer.{question_index, selected_index, time_ms?, confidence?}{is_correct, correct_index, explanation, next_question, recorded}. Owner check; 409 QUIZ_ATTEMPT_ALREADY_COMPLETED after completion; 400 QUIZ_QUESTION_INVALID on out-of-range indexes; malformed items (no correct option) never grade correct (#129 rule); next_question comes back stripped of the answer key. Idempotent on (attempt_id, question_index): re-answering returns the FIRST recorded response with recorded: false — no revision, decided and documented; a lost insert race re-reads whatever the UNIQUE let win.

C2 — quiz_responses (migration 20260812214402): attempt_id (FK cascade), question_index, selected_index, is_correct, time_ms, confidence, answered_at. All plaintext analytics scalars (#521 rationale — no free text; question/option text stays encrypted in questions_json). UNIQUE (attempt_id, question_index) is the idempotency contract; real-DB integration tests pin the UNIQUE arbitration and the cascade.

C3 — include_answer_key on generate, default true so the current QuizPanel keeps working. Every keyed response logs a deprecation breadcrumb; false strips per-option correct booleans from the response while storage keeps them for server-side grading. Flip + delete tracked in #546; removal is a hard requirement of #537.

C4 — submit reconciles. Responses recorded through C1 are the source of truth per question (a contradicting payload answer is ignored — letting the final POST override answer-time grades would reopen the client-grading hole); questions never answered through C1 fall back to the payload, so the current all-at-the-end client is unaffected. The atomic completed_at claim (PR #464) and 409 behaviour are untouched. Mixed-case and recorded-only tests included.

Wire-contract notes (current client)

  • Generate: include_answer_key is additive with a compatible default; response shape unchanged for the old client.
  • Submit: behaviour identical when no C1 responses exist (the old client never creates any).
  • New endpoint is additive.

Verification

  • Hermetic suite: 1938 passed (10 new tests, written first, watched fail).
  • Full local cycle under the stack lock: Playwright 43 → oracles exit 0 → integration lane (counts checked) → teardown.
  • ruff check clean. Migration replayed from empty by the stack bring-up; applied to staging ahead of merge.

🤖 Generated with Claude Code

…_responses, answer-key deprecation (#541)
Workstream C of the pre-revamp quiz repair batch (epic #537):
C1 — POST /api/quiz/attempts/{attempt_id}/answer grades one question
server-side: owner check, 409 after completion, 400 QUIZ_QUESTION_INVALID
on out-of-range indexes, malformed items never grade correct (#129 rule).
Idempotent on (attempt_id, question_index): re-answering returns the
FIRST recorded response with recorded:false — no revision, decided and
documented for the #537 flow. Returns is_correct/correct_index/
explanation plus the next question stripped of the answer key.
C2 — quiz_responses table (migration 20260812214402): plaintext
analytics scalars only (indexes, boolean, time_ms, confidence), UNIQUE
(attempt_id, question_index) as the idempotency contract, FK cascade
with the attempt. Real-DB integration tests pin the UNIQUE arbitration
and the cascade.
C3 — include_answer_key on generate, default true so the current
QuizPanel keeps working; every keyed response logs a deprecation
breadcrumb; false strips per-option correct booleans from the response
while storage keeps them for grading. Removal tracked in #546; deleting
the key is a hard requirement of #537.
C4 — submit prefers recorded quiz_responses per question (a
contradicting payload answer is ignored — answer-time grades are the
source of truth) and falls back to the payload for questions never
answered through C1. The atomic completed_at claim (PR #464) and 409
behaviour are untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@supabase

supabaseBot commented Aug 13, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 13, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingc2d316eCommit Preview URL

Branch Preview URL
Aug 13 2026, 02:09 AM

@coderabbitai

coderabbitaiBot commented Aug 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:57 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3676f4bd-bdde-47dd-903a-187dd2195fe4

📥 Commits

Reviewing files that changed from the base of the PR and between 5e1a322 and c2d316e.

📒 Files selected for processing (6)
  • backend/db/migrations/20260812214402_quiz_responses.sql
  • backend/models/__init__.py
  • backend/routes/quiz.py
  • backend/services/quiz_errors.py
  • backend/tests/integration/test_quiz_responses_db.py
  • backend/tests/test_quiz_answers_c.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…e index/id mismatch, drop the redundant index
Review findings (xhigh, 4 confirmed; 5 reports collapsed to one root cause):
- submit now persists the RECONCILED answer set (recorded responses
winning over payload) instead of the raw request body. A recorded-only
submit previously stored a full score beside answers_json=[], and a
contradicted payload answer was stored despite losing to the recorded
response — the attempt record disagreed with its own score.
- The answer endpoint accepts an optional question_id and rejects a
mismatch with question_index, plus echoes both in the response: passing
the 1-based wire id as the 0-based index used to silently grade the
neighbouring question, which idempotency then locked in.
- quiz_responses drops the standalone attempt_id index — the UNIQUE's
btree already leads with attempt_id, so it only added a write to the
per-answer hot path.
- correct_index is resolved once per request instead of re-scanning the
options on every _graded call (it never depended on the answer).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 73fb504 into mainAug 13, 2026
4 of 7 checks passed
Sign up for freeto 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.

quiz C: server-authoritative grading — per-question answer endpoint, quiz_responses table, answer-key deprecation

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(quiz): server-authoritative grading — per-question answers, quiz_responses, answer-key deprecation (#541) - #549

Merged
AndresL230 merged 2 commits into
mainfrom
feat/541-quiz-answer-endpoint
Aug 13, 2026
Merged

feat(quiz): server-authoritative grading — per-question answers, quiz_responses, answer-key deprecation (#541)#549
AndresL230 merged 2 commits into
mainfrom
feat/541-quiz-answer-endpoint

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

Closes#541. Workstream C of the pre-revamp quiz repair batch (epic #537). Today the answer key ships to the browser and review grades locally; every interesting version of the #537 flow (in-session adaptivity, confidence, timing, honest results) needs the server to be the one that knows. This PR gives the new client its grading API while the current client keeps working unchanged.

What

C1 — POST /api/quiz/attempts/{attempt_id}/answer.{question_index, selected_index, time_ms?, confidence?}{is_correct, correct_index, explanation, next_question, recorded}. Owner check; 409 QUIZ_ATTEMPT_ALREADY_COMPLETED after completion; 400 QUIZ_QUESTION_INVALID on out-of-range indexes; malformed items (no correct option) never grade correct (#129 rule); next_question comes back stripped of the answer key. Idempotent on (attempt_id, question_index): re-answering returns the FIRST recorded response with recorded: false — no revision, decided and documented; a lost insert race re-reads whatever the UNIQUE let win.

C2 — quiz_responses (migration 20260812214402): attempt_id (FK cascade), question_index, selected_index, is_correct, time_ms, confidence, answered_at. All plaintext analytics scalars (#521 rationale — no free text; question/option text stays encrypted in questions_json). UNIQUE (attempt_id, question_index) is the idempotency contract; real-DB integration tests pin the UNIQUE arbitration and the cascade.

C3 — include_answer_key on generate, default true so the current QuizPanel keeps working. Every keyed response logs a deprecation breadcrumb; false strips per-option correct booleans from the response while storage keeps them for server-side grading. Flip + delete tracked in #546; removal is a hard requirement of #537.

C4 — submit reconciles. Responses recorded through C1 are the source of truth per question (a contradicting payload answer is ignored — letting the final POST override answer-time grades would reopen the client-grading hole); questions never answered through C1 fall back to the payload, so the current all-at-the-end client is unaffected. The atomic completed_at claim (PR #464) and 409 behaviour are untouched. Mixed-case and recorded-only tests included.

Wire-contract notes (current client)

  • Generate: include_answer_key is additive with a compatible default; response shape unchanged for the old client.
  • Submit: behaviour identical when no C1 responses exist (the old client never creates any).
  • New endpoint is additive.

Verification

  • Hermetic suite: 1938 passed (10 new tests, written first, watched fail).
  • Full local cycle under the stack lock: Playwright 43 → oracles exit 0 → integration lane (counts checked) → teardown.
  • ruff check clean. Migration replayed from empty by the stack bring-up; applied to staging ahead of merge.

🤖 Generated with Claude Code

…_responses, answer-key deprecation (#541)
Workstream C of the pre-revamp quiz repair batch (epic #537):
C1 — POST /api/quiz/attempts/{attempt_id}/answer grades one question
server-side: owner check, 409 after completion, 400 QUIZ_QUESTION_INVALID
on out-of-range indexes, malformed items never grade correct (#129 rule).
Idempotent on (attempt_id, question_index): re-answering returns the
FIRST recorded response with recorded:false — no revision, decided and
documented for the #537 flow. Returns is_correct/correct_index/
explanation plus the next question stripped of the answer key.
C2 — quiz_responses table (migration 20260812214402): plaintext
analytics scalars only (indexes, boolean, time_ms, confidence), UNIQUE
(attempt_id, question_index) as the idempotency contract, FK cascade
with the attempt. Real-DB integration tests pin the UNIQUE arbitration
and the cascade.
C3 — include_answer_key on generate, default true so the current
QuizPanel keeps working; every keyed response logs a deprecation
breadcrumb; false strips per-option correct booleans from the response
while storage keeps them for grading. Removal tracked in #546; deleting
the key is a hard requirement of #537.
C4 — submit prefers recorded quiz_responses per question (a
contradicting payload answer is ignored — answer-time grades are the
source of truth) and falls back to the payload for questions never
answered through C1. The atomic completed_at claim (PR #464) and 409
behaviour are untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@supabase

supabaseBot commented Aug 13, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 13, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingc2d316eCommit Preview URL

Branch Preview URL
Aug 13 2026, 02:09 AM

@coderabbitai

coderabbitaiBot commented Aug 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:57 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3676f4bd-bdde-47dd-903a-187dd2195fe4

📥 Commits

Reviewing files that changed from the base of the PR and between 5e1a322 and c2d316e.

📒 Files selected for processing (6)
  • backend/db/migrations/20260812214402_quiz_responses.sql
  • backend/models/__init__.py
  • backend/routes/quiz.py
  • backend/services/quiz_errors.py
  • backend/tests/integration/test_quiz_responses_db.py
  • backend/tests/test_quiz_answers_c.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…e index/id mismatch, drop the redundant index
Review findings (xhigh, 4 confirmed; 5 reports collapsed to one root cause):
- submit now persists the RECONCILED answer set (recorded responses
winning over payload) instead of the raw request body. A recorded-only
submit previously stored a full score beside answers_json=[], and a
contradicted payload answer was stored despite losing to the recorded
response — the attempt record disagreed with its own score.
- The answer endpoint accepts an optional question_id and rejects a
mismatch with question_index, plus echoes both in the response: passing
the 1-based wire id as the 0-based index used to silently grade the
neighbouring question, which idempotency then locked in.
- quiz_responses drops the standalone attempt_id index — the UNIQUE's
btree already leads with attempt_id, so it only added a write to the
per-answer hot path.
- correct_index is resolved once per request instead of re-scanning the
options on every _graded call (it never depended on the answer).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 73fb504 into mainAug 13, 2026
4 of 7 checks passed
Sign up for freeto 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.

quiz C: server-authoritative grading — per-question answer endpoint, quiz_responses table, answer-key deprecation

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(quiz): server-authoritative grading — per-question answers, quiz_responses, answer-key deprecation (#541) - #549

Merged
AndresL230 merged 2 commits into
mainfrom
feat/541-quiz-answer-endpoint
Aug 13, 2026
Merged

feat(quiz): server-authoritative grading — per-question answers, quiz_responses, answer-key deprecation (#541)#549
AndresL230 merged 2 commits into
mainfrom
feat/541-quiz-answer-endpoint

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

Closes#541. Workstream C of the pre-revamp quiz repair batch (epic #537). Today the answer key ships to the browser and review grades locally; every interesting version of the #537 flow (in-session adaptivity, confidence, timing, honest results) needs the server to be the one that knows. This PR gives the new client its grading API while the current client keeps working unchanged.

What

C1 — POST /api/quiz/attempts/{attempt_id}/answer.{question_index, selected_index, time_ms?, confidence?}{is_correct, correct_index, explanation, next_question, recorded}. Owner check; 409 QUIZ_ATTEMPT_ALREADY_COMPLETED after completion; 400 QUIZ_QUESTION_INVALID on out-of-range indexes; malformed items (no correct option) never grade correct (#129 rule); next_question comes back stripped of the answer key. Idempotent on (attempt_id, question_index): re-answering returns the FIRST recorded response with recorded: false — no revision, decided and documented; a lost insert race re-reads whatever the UNIQUE let win.

C2 — quiz_responses (migration 20260812214402): attempt_id (FK cascade), question_index, selected_index, is_correct, time_ms, confidence, answered_at. All plaintext analytics scalars (#521 rationale — no free text; question/option text stays encrypted in questions_json). UNIQUE (attempt_id, question_index) is the idempotency contract; real-DB integration tests pin the UNIQUE arbitration and the cascade.

C3 — include_answer_key on generate, default true so the current QuizPanel keeps working. Every keyed response logs a deprecation breadcrumb; false strips per-option correct booleans from the response while storage keeps them for server-side grading. Flip + delete tracked in #546; removal is a hard requirement of #537.

C4 — submit reconciles. Responses recorded through C1 are the source of truth per question (a contradicting payload answer is ignored — letting the final POST override answer-time grades would reopen the client-grading hole); questions never answered through C1 fall back to the payload, so the current all-at-the-end client is unaffected. The atomic completed_at claim (PR #464) and 409 behaviour are untouched. Mixed-case and recorded-only tests included.

Wire-contract notes (current client)

  • Generate: include_answer_key is additive with a compatible default; response shape unchanged for the old client.
  • Submit: behaviour identical when no C1 responses exist (the old client never creates any).
  • New endpoint is additive.

Verification

  • Hermetic suite: 1938 passed (10 new tests, written first, watched fail).
  • Full local cycle under the stack lock: Playwright 43 → oracles exit 0 → integration lane (counts checked) → teardown.
  • ruff check clean. Migration replayed from empty by the stack bring-up; applied to staging ahead of merge.

🤖 Generated with Claude Code

…_responses, answer-key deprecation (#541)
Workstream C of the pre-revamp quiz repair batch (epic #537):
C1 — POST /api/quiz/attempts/{attempt_id}/answer grades one question
server-side: owner check, 409 after completion, 400 QUIZ_QUESTION_INVALID
on out-of-range indexes, malformed items never grade correct (#129 rule).
Idempotent on (attempt_id, question_index): re-answering returns the
FIRST recorded response with recorded:false — no revision, decided and
documented for the #537 flow. Returns is_correct/correct_index/
explanation plus the next question stripped of the answer key.
C2 — quiz_responses table (migration 20260812214402): plaintext
analytics scalars only (indexes, boolean, time_ms, confidence), UNIQUE
(attempt_id, question_index) as the idempotency contract, FK cascade
with the attempt. Real-DB integration tests pin the UNIQUE arbitration
and the cascade.
C3 — include_answer_key on generate, default true so the current
QuizPanel keeps working; every keyed response logs a deprecation
breadcrumb; false strips per-option correct booleans from the response
while storage keeps them for grading. Removal tracked in #546; deleting
the key is a hard requirement of #537.
C4 — submit prefers recorded quiz_responses per question (a
contradicting payload answer is ignored — answer-time grades are the
source of truth) and falls back to the payload for questions never
answered through C1. The atomic completed_at claim (PR #464) and 409
behaviour are untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@supabase

supabaseBot commented Aug 13, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 13, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingc2d316eCommit Preview URL

Branch Preview URL
Aug 13 2026, 02:09 AM

@coderabbitai

coderabbitaiBot commented Aug 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:57 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3676f4bd-bdde-47dd-903a-187dd2195fe4

📥 Commits

Reviewing files that changed from the base of the PR and between 5e1a322 and c2d316e.

📒 Files selected for processing (6)
  • backend/db/migrations/20260812214402_quiz_responses.sql
  • backend/models/__init__.py
  • backend/routes/quiz.py
  • backend/services/quiz_errors.py
  • backend/tests/integration/test_quiz_responses_db.py
  • backend/tests/test_quiz_answers_c.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…e index/id mismatch, drop the redundant index
Review findings (xhigh, 4 confirmed; 5 reports collapsed to one root cause):
- submit now persists the RECONCILED answer set (recorded responses
winning over payload) instead of the raw request body. A recorded-only
submit previously stored a full score beside answers_json=[], and a
contradicted payload answer was stored despite losing to the recorded
response — the attempt record disagreed with its own score.
- The answer endpoint accepts an optional question_id and rejects a
mismatch with question_index, plus echoes both in the response: passing
the 1-based wire id as the 0-based index used to silently grade the
neighbouring question, which idempotency then locked in.
- quiz_responses drops the standalone attempt_id index — the UNIQUE's
btree already leads with attempt_id, so it only added a write to the
per-answer hot path.
- correct_index is resolved once per request instead of re-scanning the
options on every _graded call (it never depended on the answer).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 73fb504 into mainAug 13, 2026
4 of 7 checks passed
Sign up for freeto 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.

quiz C: server-authoritative grading — per-question answer endpoint, quiz_responses table, answer-key deprecation

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(quiz): server-authoritative grading — per-question answers, quiz_responses, answer-key deprecation (#541) - #549

Merged
AndresL230 merged 2 commits into
mainfrom
feat/541-quiz-answer-endpoint
Aug 13, 2026
Merged

feat(quiz): server-authoritative grading — per-question answers, quiz_responses, answer-key deprecation (#541)#549
AndresL230 merged 2 commits into
mainfrom
feat/541-quiz-answer-endpoint

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

Closes#541. Workstream C of the pre-revamp quiz repair batch (epic #537). Today the answer key ships to the browser and review grades locally; every interesting version of the #537 flow (in-session adaptivity, confidence, timing, honest results) needs the server to be the one that knows. This PR gives the new client its grading API while the current client keeps working unchanged.

What

C1 — POST /api/quiz/attempts/{attempt_id}/answer.{question_index, selected_index, time_ms?, confidence?}{is_correct, correct_index, explanation, next_question, recorded}. Owner check; 409 QUIZ_ATTEMPT_ALREADY_COMPLETED after completion; 400 QUIZ_QUESTION_INVALID on out-of-range indexes; malformed items (no correct option) never grade correct (#129 rule); next_question comes back stripped of the answer key. Idempotent on (attempt_id, question_index): re-answering returns the FIRST recorded response with recorded: false — no revision, decided and documented; a lost insert race re-reads whatever the UNIQUE let win.

C2 — quiz_responses (migration 20260812214402): attempt_id (FK cascade), question_index, selected_index, is_correct, time_ms, confidence, answered_at. All plaintext analytics scalars (#521 rationale — no free text; question/option text stays encrypted in questions_json). UNIQUE (attempt_id, question_index) is the idempotency contract; real-DB integration tests pin the UNIQUE arbitration and the cascade.

C3 — include_answer_key on generate, default true so the current QuizPanel keeps working. Every keyed response logs a deprecation breadcrumb; false strips per-option correct booleans from the response while storage keeps them for server-side grading. Flip + delete tracked in #546; removal is a hard requirement of #537.

C4 — submit reconciles. Responses recorded through C1 are the source of truth per question (a contradicting payload answer is ignored — letting the final POST override answer-time grades would reopen the client-grading hole); questions never answered through C1 fall back to the payload, so the current all-at-the-end client is unaffected. The atomic completed_at claim (PR #464) and 409 behaviour are untouched. Mixed-case and recorded-only tests included.

Wire-contract notes (current client)

  • Generate: include_answer_key is additive with a compatible default; response shape unchanged for the old client.
  • Submit: behaviour identical when no C1 responses exist (the old client never creates any).
  • New endpoint is additive.

Verification

  • Hermetic suite: 1938 passed (10 new tests, written first, watched fail).
  • Full local cycle under the stack lock: Playwright 43 → oracles exit 0 → integration lane (counts checked) → teardown.
  • ruff check clean. Migration replayed from empty by the stack bring-up; applied to staging ahead of merge.

🤖 Generated with Claude Code

…_responses, answer-key deprecation (#541)
Workstream C of the pre-revamp quiz repair batch (epic #537):
C1 — POST /api/quiz/attempts/{attempt_id}/answer grades one question
server-side: owner check, 409 after completion, 400 QUIZ_QUESTION_INVALID
on out-of-range indexes, malformed items never grade correct (#129 rule).
Idempotent on (attempt_id, question_index): re-answering returns the
FIRST recorded response with recorded:false — no revision, decided and
documented for the #537 flow. Returns is_correct/correct_index/
explanation plus the next question stripped of the answer key.
C2 — quiz_responses table (migration 20260812214402): plaintext
analytics scalars only (indexes, boolean, time_ms, confidence), UNIQUE
(attempt_id, question_index) as the idempotency contract, FK cascade
with the attempt. Real-DB integration tests pin the UNIQUE arbitration
and the cascade.
C3 — include_answer_key on generate, default true so the current
QuizPanel keeps working; every keyed response logs a deprecation
breadcrumb; false strips per-option correct booleans from the response
while storage keeps them for grading. Removal tracked in #546; deleting
the key is a hard requirement of #537.
C4 — submit prefers recorded quiz_responses per question (a
contradicting payload answer is ignored — answer-time grades are the
source of truth) and falls back to the payload for questions never
answered through C1. The atomic completed_at claim (PR #464) and 409
behaviour are untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@supabase

supabaseBot commented Aug 13, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 13, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingc2d316eCommit Preview URL

Branch Preview URL
Aug 13 2026, 02:09 AM

@coderabbitai

coderabbitaiBot commented Aug 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:57 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3676f4bd-bdde-47dd-903a-187dd2195fe4

📥 Commits

Reviewing files that changed from the base of the PR and between 5e1a322 and c2d316e.

📒 Files selected for processing (6)
  • backend/db/migrations/20260812214402_quiz_responses.sql
  • backend/models/__init__.py
  • backend/routes/quiz.py
  • backend/services/quiz_errors.py
  • backend/tests/integration/test_quiz_responses_db.py
  • backend/tests/test_quiz_answers_c.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…e index/id mismatch, drop the redundant index
Review findings (xhigh, 4 confirmed; 5 reports collapsed to one root cause):
- submit now persists the RECONCILED answer set (recorded responses
winning over payload) instead of the raw request body. A recorded-only
submit previously stored a full score beside answers_json=[], and a
contradicted payload answer was stored despite losing to the recorded
response — the attempt record disagreed with its own score.
- The answer endpoint accepts an optional question_id and rejects a
mismatch with question_index, plus echoes both in the response: passing
the 1-based wire id as the 0-based index used to silently grade the
neighbouring question, which idempotency then locked in.
- quiz_responses drops the standalone attempt_id index — the UNIQUE's
btree already leads with attempt_id, so it only added a write to the
per-answer hot path.
- correct_index is resolved once per request instead of re-scanning the
options on every _graded call (it never depended on the answer).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 73fb504 into mainAug 13, 2026
4 of 7 checks passed
Sign up for freeto 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.

quiz C: server-authoritative grading — per-question answer endpoint, quiz_responses table, answer-key deprecation

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(quiz): server-authoritative grading — per-question answers, quiz_responses, answer-key deprecation (#541) - #549

Merged
AndresL230 merged 2 commits into
mainfrom
feat/541-quiz-answer-endpoint
Aug 13, 2026
Merged

feat(quiz): server-authoritative grading — per-question answers, quiz_responses, answer-key deprecation (#541)#549
AndresL230 merged 2 commits into
mainfrom
feat/541-quiz-answer-endpoint

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

Closes#541. Workstream C of the pre-revamp quiz repair batch (epic #537). Today the answer key ships to the browser and review grades locally; every interesting version of the #537 flow (in-session adaptivity, confidence, timing, honest results) needs the server to be the one that knows. This PR gives the new client its grading API while the current client keeps working unchanged.

What

C1 — POST /api/quiz/attempts/{attempt_id}/answer.{question_index, selected_index, time_ms?, confidence?}{is_correct, correct_index, explanation, next_question, recorded}. Owner check; 409 QUIZ_ATTEMPT_ALREADY_COMPLETED after completion; 400 QUIZ_QUESTION_INVALID on out-of-range indexes; malformed items (no correct option) never grade correct (#129 rule); next_question comes back stripped of the answer key. Idempotent on (attempt_id, question_index): re-answering returns the FIRST recorded response with recorded: false — no revision, decided and documented; a lost insert race re-reads whatever the UNIQUE let win.

C2 — quiz_responses (migration 20260812214402): attempt_id (FK cascade), question_index, selected_index, is_correct, time_ms, confidence, answered_at. All plaintext analytics scalars (#521 rationale — no free text; question/option text stays encrypted in questions_json). UNIQUE (attempt_id, question_index) is the idempotency contract; real-DB integration tests pin the UNIQUE arbitration and the cascade.

C3 — include_answer_key on generate, default true so the current QuizPanel keeps working. Every keyed response logs a deprecation breadcrumb; false strips per-option correct booleans from the response while storage keeps them for server-side grading. Flip + delete tracked in #546; removal is a hard requirement of #537.

C4 — submit reconciles. Responses recorded through C1 are the source of truth per question (a contradicting payload answer is ignored — letting the final POST override answer-time grades would reopen the client-grading hole); questions never answered through C1 fall back to the payload, so the current all-at-the-end client is unaffected. The atomic completed_at claim (PR #464) and 409 behaviour are untouched. Mixed-case and recorded-only tests included.

Wire-contract notes (current client)

  • Generate: include_answer_key is additive with a compatible default; response shape unchanged for the old client.
  • Submit: behaviour identical when no C1 responses exist (the old client never creates any).
  • New endpoint is additive.

Verification

  • Hermetic suite: 1938 passed (10 new tests, written first, watched fail).
  • Full local cycle under the stack lock: Playwright 43 → oracles exit 0 → integration lane (counts checked) → teardown.
  • ruff check clean. Migration replayed from empty by the stack bring-up; applied to staging ahead of merge.

🤖 Generated with Claude Code

…_responses, answer-key deprecation (#541)
Workstream C of the pre-revamp quiz repair batch (epic #537):
C1 — POST /api/quiz/attempts/{attempt_id}/answer grades one question
server-side: owner check, 409 after completion, 400 QUIZ_QUESTION_INVALID
on out-of-range indexes, malformed items never grade correct (#129 rule).
Idempotent on (attempt_id, question_index): re-answering returns the
FIRST recorded response with recorded:false — no revision, decided and
documented for the #537 flow. Returns is_correct/correct_index/
explanation plus the next question stripped of the answer key.
C2 — quiz_responses table (migration 20260812214402): plaintext
analytics scalars only (indexes, boolean, time_ms, confidence), UNIQUE
(attempt_id, question_index) as the idempotency contract, FK cascade
with the attempt. Real-DB integration tests pin the UNIQUE arbitration
and the cascade.
C3 — include_answer_key on generate, default true so the current
QuizPanel keeps working; every keyed response logs a deprecation
breadcrumb; false strips per-option correct booleans from the response
while storage keeps them for grading. Removal tracked in #546; deleting
the key is a hard requirement of #537.
C4 — submit prefers recorded quiz_responses per question (a
contradicting payload answer is ignored — answer-time grades are the
source of truth) and falls back to the payload for questions never
answered through C1. The atomic completed_at claim (PR #464) and 409
behaviour are untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@supabase

supabaseBot commented Aug 13, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 13, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingc2d316eCommit Preview URL

Branch Preview URL
Aug 13 2026, 02:09 AM

@coderabbitai

coderabbitaiBot commented Aug 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:57 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3676f4bd-bdde-47dd-903a-187dd2195fe4

📥 Commits

Reviewing files that changed from the base of the PR and between 5e1a322 and c2d316e.

📒 Files selected for processing (6)
  • backend/db/migrations/20260812214402_quiz_responses.sql
  • backend/models/__init__.py
  • backend/routes/quiz.py
  • backend/services/quiz_errors.py
  • backend/tests/integration/test_quiz_responses_db.py
  • backend/tests/test_quiz_answers_c.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…e index/id mismatch, drop the redundant index
Review findings (xhigh, 4 confirmed; 5 reports collapsed to one root cause):
- submit now persists the RECONCILED answer set (recorded responses
winning over payload) instead of the raw request body. A recorded-only
submit previously stored a full score beside answers_json=[], and a
contradicted payload answer was stored despite losing to the recorded
response — the attempt record disagreed with its own score.
- The answer endpoint accepts an optional question_id and rejects a
mismatch with question_index, plus echoes both in the response: passing
the 1-based wire id as the 0-based index used to silently grade the
neighbouring question, which idempotency then locked in.
- quiz_responses drops the standalone attempt_id index — the UNIQUE's
btree already leads with attempt_id, so it only added a write to the
per-answer hot path.
- correct_index is resolved once per request instead of re-scanning the
options on every _graded call (it never depended on the answer).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 73fb504 into mainAug 13, 2026
4 of 7 checks passed
Sign up for freeto 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.

quiz C: server-authoritative grading — per-question answer endpoint, quiz_responses table, answer-key deprecation

1 participant

@AndresL230