Skip to content

feat(quiz): inline code context per question (anchors) - #26

Merged
jonasbrami merged 2 commits into
mainfrom
feat/anchors
May 29, 2026
Merged

feat(quiz): inline code context per question (anchors)#26
jonasbrami merged 2 commits into
mainfrom
feat/anchors

Conversation

@jonasbrami

@jonasbrami jonasbrami commented May 29, 2026

Copy link
Copy Markdown
Owner

What

Adds an optional anchor on every question so the browser shows the exact diff hunk inline, next to the question. The reader no longer has to hold the diff in their head or switch to GitHub.

Changes

  • models — new Anchor {path, start_line, end_line} + optional anchor field on all four question types. Optional (defaults to None) → old cached quizzes load unchanged; the range is validated (start ≤ end, lines ≥ 1), but a path need not be in the diff (a question may anchor surrounding context).
  • webGET /diff?path= serves one file's unified-diff section. _DiffProvider is hoisted into main() and shared by the MCP tools and the web app (one fetch/cache). Returns 503 when no diff source is wired.
  • frontend — a collapsible per-question code panel, fetched on first expand and rendered DOM-built (textContent only, never innerHTML); +/- lines and @@ headers styled via CSS classes. Shown on both the answering and results-review cards; absent when a question has no anchor.
  • generation prompt — instructs the author to emit a tight anchor whenever a question probes specific lines. Never affects grading.

Invariants preserved

  • Browser ↔ host still only via QuizState (the /diff read is browser↔web, in-process).
  • Built-in host tool confinement (Read/Grep/Glob) unchanged.
  • textContent-only frontend; diff hunks are DOM-built, not string-injected.
  • Backward-compatible: questions/quizzes without anchor work exactly as before.

Testing

  • Unit: Anchor round-trip, single-line range, reversed-range + non-positive rejections, every type carries an optional anchor, and backward-compat (no-anchor questions + a legacy quiz dict load with anchor=None).
  • Web: GET /diff hit + miss message + 503 when unavailable.
  • Full mcp/engine suite green; ruff + ruff format --check + mypy --strict clean.
  • Real-browser check (Chrome DevTools): panel renders, expands, loads the hunk over /diff, color-codes +/-/@@ lines, and is correctly absent on an anchorless open question.

🤖 Generated with Claude Code

Adds an optional `anchor: {path, start_line, end_line}` to every question type
so the browser can show the exact diff hunk inline next to the question — the
reader no longer has to hold the diff in their head or switch to GitHub.

- models: new `Anchor` model + optional `anchor` field on all four question
  types. Optional (defaults to None) → old cached quizzes load unchanged; the
  range is validated (start ≤ end, lines ≥ 1) but a path need not be in the diff
  (a question may anchor surrounding context).
- web: `GET /diff?path=` serves one file's unified-diff section; `_DiffProvider`
  is hoisted into `main()` and shared by the MCP tools and the web app (one
  fetch/cache). 503 when no diff source is wired.
- frontend: collapsible per-question code panel, fetched on first expand and
  rendered DOM-built (textContent only, never innerHTML); +/- lines and `@@`
  headers are styled via CSS classes. Present on both the answering and the
  results-review cards; absent when a question has no anchor.
- generation prompt: instructs the author to emit a tight `anchor` whenever a
  question probes specific lines; never affects grading.

Foundation for the diff coverage map (#9) and drill-this (#5).

Tested: models round-trip + backward-compat (no anchor) unit tests; /diff
endpoint tests (hit + miss + 503); full mcp/engine suite green (ruff, mypy
strict). Anchor UI verified in a real browser (panel renders, expands, loads
the hunk, color-codes lines; absent on anchorless questions).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jonasbrami
jonasbrami changed the base branch from docs/learning-ux-roadmap to main May 29, 2026 06:08
Review feedback on the anchor UI:

- The panel showed the whole file's diff; the anchor's line range was only a
  cosmetic label. Now the diff is parsed into hunks and scoped to the hunk(s)
  overlapping the anchor's new-side range (falls back to all hunks if none
  overlap), the anchored new-side lines are highlighted, and the file-level
  header lines (diff --git/index/---/+++) are dropped as noise.
- A `/diff` miss (renamed file, or a binary/minified file filtered out of the
  diff) returned the "No changed file matches…" sentence with 200, which the UI
  rendered as a fake one-line diff. Now the UI shows a clean "Not part of the
  PR diff." note instead.
- loadHunk now logs the error on a fetch failure (was silently swallowed).
- Added a guard test that a question's anchor survives set_quiz + snapshot.

Verified in a real browser: a question anchored to the 2nd hunk shows only that
hunk with its added lines highlighted; a question anchored to a non-diff path
shows the note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jonasbrami
jonasbrami merged commit 717bab2 into main May 29, 2026
2 checks passed
jonasbrami added a commit that referenced this pull request May 29, 2026
Shows which of the PR's changed files have a question anchored to them, so the
reader can see at a glance what the quiz does and doesn't probe.

- web: `GET /changed-files` returns the PR's changed-file paths (`changed_files`
  callable, wired in server.py to the shared `_DiffProvider`). 503 when unwired.
- frontend: a "Diff coverage" sidebar block (answering view) listing each changed
  file with a covered/uncovered marker + an "N of M files probed" count. Coverage
  is computed client-side by matching question anchors to changed files with the
  same exact / repo-relative-suffix / basename logic the server's do_file_diff uses.

Display only — the "ask the host to cover this file" steer is Track B. Builds on
the anchors feature (#26).

Tested: /changed-files endpoint (list + 503). Verified in a real browser: exact and
basename-only anchors both mark their file covered; files with no anchored question
show uncovered; count is correct.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant