pdf-quiz-generator/docs/TODO.md
Daniel ddd8c4c4e3 feat: the session moves along the difficulty range, per topic
The half of "adaptive" that was never built. Difficulty was a filter a learner
could set and nothing the session did on its own, so somebody at 30% on a topic
and somebody at 90% were asked the same questions in the same order.

A question's value is now scaled by how far its level is from the one the
learner's readiness on *that topic* calls for: under 0.45 wants easy, up to
0.78 medium, above that hard. Per topic because strong on growth and weak on
arrhythmias is the normal case and one number across the two describes nobody.

A multiplier and not a filter, for the same reason the reranker is a
permutation: thinning the bank to one level makes it three times smaller, and
on a narrow topic that is the same eight questions every time. One step away
keeps 0.72 of its value, two steps 0.5, and an unlabelled question 0.92 —
unknown is not wrong.

The first band's edge is 0.45 rather than 0.55 so that a topic with no evidence
— which sits at exactly NEUTRAL_RECALL — gets medium questions. Handing
somebody the easiest questions in a subject nobody has measured them on is a
poor way to find out what they know.

Written up in docs/adaptive-sessions.md, which now says five rules.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
2026-09-12 21:39:23 +02:00

44 KiB
Raw Permalink Blame History

PedsHub — outstanding work

Everything requested and not yet delivered. Ordered roughly by dependency, not priority — say which to take and I'll reorder.

Updated 2026-09-11 (overnight).


Asked for on 2026-09-11, not yet done

Captured so nothing is lost while the article writing runs.

Sessions and analysis

  • Session analysis after a quiz/analysis/session/:attemptId: rail of latest sessions, the four figures (correct, completed, time per question, total time), a donut, study recommendations, and a paginated performance table with time and peer statistics. Time per question is now recorded (attempt_answers.seconds_spent); answers from before that read "—" rather than claiming zero.
  • Return to a session rather than being launched into it/quizzes/:id auto-started, so every link from the history fired a 243-question exam on click. It now shows the session — mode, length, what the clock does — and starts when asked. Session rows and the analysis rail open the session's analysis rather than the raw answer list.
  • An unsuspended exam keeps running — done 2026-09-11. When the clock runs out the exam is submitted with what was answered and the score counts; resuming lands on the result. Settled by whichever notices first (resume, or the sessions list). Previously such attempts were flagged expired and hidden from every statistic. Suspending still holds the clock.
  • Deleting a session removes its data — done 2026-09-11. Answers cascade; saved progress and the device lock are cleared; a study-plan block whose only completed attempt is deleted goes back to unfinished.
  • Reset all data — done 2026-09-11. Settings → Your data. Typed RESET, states what goes (sessions, answers, saved progress, plan progress, reading marks, saved questions, question notes) and what stays (account, anything authored, AI conversations); reports the counts afterwards.
  • [~] Third person, impersonal — 819 imperatives across 348 sections. Nine of ten batches applied (740 imperatives cleared, 79 left in 43 sections); one batch outstanding. scripts/prose_pass.py exports, takes rewrites back, and refuses any that lost a cross-reference, changed a number or landed outside 0.7-1.4x length. First and second person were measured at zero and sixteen (sample boilerplate) — neither needed work.
  • Smart cross-linking — done 2026-09-11. First mention per view, lists as jump lists, the 23 titles in more than 5% of articles link from lists only. 3,560 links to 2,191, and the tail with it: Seizures 119 to 48. Re-runnable — it strips its own work and reapplies the rule.
  • 37 nested cross-references repaired[[363|[[245|reflux]] disease]] from the first linker pass, which rendered as literal brackets.
  • Clinical view in MDM order — done 2026-09-11. All 323 clinical views now read Clinical paths → Diagnosis → Management → Prognosis and outcome (the last only where the source said something real about course; 95 of 323 got one). It was 413 free-form sections under 242 different titles. scripts/mdm_pass.py does the moving and refuses any article that lost a cross-reference, changed a fact, or came back outside 0.8-1.3x.
  • Pediatric Dentistry's placeholder clinical view removed — done 2026-09-12. Neither an educator nor a delete: the article is a discipline overview whose long and high-yield views are real content about the discipline, and only its clinical view was three sections announcing that the source held no medicine. The article stays and the placeholder view goes, so the reader is offered two views instead of three that include a disclaimer. mdm_pass check reports nothing.
  • The 11 articles missing a view now have one — done 2026-09-12. The note said 10 with no clinical view and 13 with no high-yield; the real numbers on the day were 8 and 11. The eight hand-written originals (Bronchiolitis, Acute otitis media, Kawasaki disease, Type 1 diabetes, Iron deficiency anemia, Urinary tract infection, Asthma in children, Developmental milestones) had no variant at all and read as the long view; the three discipline overviews had long and clinical but no high-yield. scripts/write_missing_views.py wrote 33 new sections from each article's own body — no number, dose or threshold appears that the article does not already state — and stamped the legacy sections long. Clinical views are in MDM order, so mdm_pass check reads 330 of 331 with a clinical view, all conforming. Every article now has a high-yield view.

Reading and study

  • Recommendations by Articles / Disciplines / Systems — done 2026-09-11. Three groupings over the same answers; Articles is the default. Systems rolls up through the symptom keyword's parent and states its own coverage (1,502 of 2,948 questions). "Practise this topic" works on whichever axis the row came from.
  • Questions filed at system level, promoted — done 2026-09-12. The taxonomy rebuild regressed a previously-closed item: merges moved 108 questions up onto a root topic. All 108 promoted with zero ambiguous cases; the remaining 146 sit at system level because no condition inside that system exists to hold them, which is a classification gap rather than a filing error. Radial Head Subluxation merged into Nursemaid's Elbow (Radial Head Subluxation) — one idea under two names.
  • Adaptive session: the difficulty ladder — done 2026-09-12, hours after being closed as unbuildable, because the reason it was unbuildable went away: every one of the 2,924 questions now carries a difficulty (622 easy, 1,634 medium, 668 hard, no failures), labelled in batches of 25 against a written rubric by classify_question_difficulty. The ordering now moves a session along the range as a learner does well or badly: difficulty_fit() scores a question by how far its level is from the one the learner's readiness on that topic calls for, per topic rather than overall, as a multiplier rather than a filter. Written up in docs/adaptive-sessions.md.
  • Adaptive session (the other halves) — closed 2026-09-12. Shrunk readiness: already done and now pinned by a test — CandidateRanking.accuracy() pulls a topic towards NEUTRAL_RECALL by PRIOR_ANSWERS, and the recommendations page sorts on readiness and falls back to raw accuracy only before the unlock threshold, which is deliberate. Difficulty as a dimension: cannot be built, and the reason is measured — all 2,924 questions have a NULL difficulty, and the empirical route is no better at 788 answers over 706 questions from 5 learners, about one answer each. A ladder scored against that column would be scoring noise while looking as though it worked. What was built instead is honesty in the control: the Difficulty facet now counts each level under the other filters and disables one that would empty the bank, so nobody picks Hard and watches the count fall to zero. That column is now written — see the reopened item above.

Settings and access

  • Settings revamp — done 2026-09-11. A section list beside one panel, the section in the URL so it can be linked to. The exam objective moved in; the notifications panel went, its one control having switched a reminder scheduler that no longer exists.
  • Access as a tree — done 2026-09-11. /access: a person on the left, everything they hold on the right. A granted branch shows its children as covered rather than separately tickable. "Everything" is the moderator role and says so, rather than a wildcard grant nobody could audit. A branch now also covers the articles filed under it — an educator could previously edit a branch's questions but not its reading.
  • Question folders — done 2026-09-12. A folder is a hand-picked set of questions and the fourth thing a grant can name, beside exam, discipline and category. Not user_collections with sharing bolted on: a library is a consequence of access (you save what you can already see, and the API says private: True about every row), a folder is a source of it, and one table holding thousands of private lists beside a handful that confer permission is a flag away from a leak. Built from the question manager — select, Add to folder — and granted on /access. Membership stays with the owner and moderators, so a grantee cannot widen their own reach; deleting a folder takes its grants with it.

Media

  • Audio and video — a murmur cannot be shown as a picture. Uploads take MP3/WAV/OGG/M4A and MP4/WebM/MOV alongside images, with their own 60 MB ceiling, and each renders as what it is.
  • Editing opens over the grid, not inside a cell that stretches its column and shoves the neighbours out of line.
  • Every figure named by its subject, with the question number appended only where a subject repeats — a legend a reader sees in an explanation should not be twelve identical words.
  • Deleting a figure in use is refused unless forced, with the count shown. Renaming and moving are safe because the link is the id.
  • Library filter in the picker is in; a library rail in the picker would be better on a wide screen.

Editor and figures

  • Rich editing on the question page — no new platform needed: Milkdown is already installed and used for articles, courses and the quick modal.
  • Formatting on the stem, options and explanation — done 2026-09-11, but NOT with Milkdown, and the check is why. Round-tripping a stem through it showed: bullets come back as * with blank lines inserted, tables are repadded, and [[id|Title]] is escaped to \[\[…]]. The first two reflow text nobody edited, which moves every saved highlight offset; the third breaks cross-references outright (fixed separately, since ArticleEditor already used Milkdown). A toolbar over the plain textarea gives the same buttons and changes nothing it was not asked to.
  • Many figures per questionquestion_media links a question to any number of images in the bank, each with a role (stem or explanation), a label the prose can refer to ("Figure 1") and an order. The 346 existing single paths were backfilled and retitled; the filename stays in the caption as provenance.
  • Explanation figures as labelled thumbnails — done 2026-09-11. They open full size and page between each other; a stem figure stays full width. Withheld until answers are revealed, like the explanation itself.
  • Figure management in the question editor — done 2026-09-11. Add from the bank, name, caption, reorder, remove, per role. An uncaptioned figure is called out as one nobody will find.

Dashboard

  • Stats are about questions, not quizzes — questions seen out of the bank, answered correctly, average score.
  • The old performance card is gone — a per-quiz graph needing two attempts, superseded by the session analysis.
  • Vary the greeting — done 2026-09-11. Four time bands including the small hours, keyed on the date so it holds for a visit and differs between days rather than changing under the reader mid-render.

Questions I owe an answer to

  • What extracted the PDFs? PyMuPDF (fitz) in pdf_service.py, with an MD5 skip list for repeated branding images. It pulled every embedded image from all 18 source PDFs, which is why one 767-page document produced 908.
  • How do the study recommendations work? — read and written up; see docs/adaptive-sessions.md, which the prepared-session work rewrote.
  • How would an adaptive session work? — done 2026-09-12: prepared sessions, with recency decay and a plan shown before it is committed. See docs/adaptive-sessions.md.
  • AI Mode (RAG chat) — done 2026-09-10. conversations and conversation_messages (migration a5b6c7d8e9f0), retrieval across sections, articles, questions and cards with the curated-link boost, the ID-citation contract enforced server-side, and /ai with a thread rail. Answers cite by number and every source opens; a section citation deep-links into its section. Question sources carry the stem only, so a chat cannot hand away the practice it is meant to prepare for.
  • Global search page — done 2026-09-10. GET /search runs every corpus at once and /search groups the answer by kind, with section hits listed under the article they belong to and linked to that section. A header box offers "go to this article" or "search everything". Each corpus keeps its own visibility rules — bank predicate and exam scope for questions, the draft rule for articles, deck ownership for cards, library grants for images. The Search / AI Mode toggle waits on AI Mode.

UI fixes raised 2026-09-10

  • Quiz/test categories removed — done.
  • Sessions list shows only a few — done, with a link to full history.
  • Analysis session rail full-height — done.
  • Articles page layout — done 2026-09-10. The page is the column browser itself: topics and the articles filed under them share a column, separated by icon, one column per level opened.
  • Systems facet duplicates — done 2026-09-10. 491 redundant "(Parent)" suffixes stripped and 3 sibling pairs merged. Nesting still to do.
  • "⚙ Filters2948 questions" — done. The stylesheet was never imported.
  • Category page relationships — direct vs rolled-up counts, empty-leaf badge, and a desktop hint on small screens.
  • Newly created categories now appear — the bare path 307-redirected to http://, which the browser blocks as mixed content, so the call failed silently. Trailing slash added.
  • Image/media page — done 2026-09-10. Tags and captions show in the picker as well as the bank, libraries have their own rail, and ids sit on every thumbnail rather than only on hover.

Content and editing

  • Study plans have a front end at all — done 2026-09-10. 13 plans were seeded with an API to serve them and no page that called it. /study-plans lists them with progress in blocks; /study-plans/:id is one plan.
  • Admin can edit study plans — done 2026-09-10. Create (as a draft), rename, publish/unpublish, delete; add, rename, reorder and remove blocks; move questions between blocks of the same plan; attach and detach reading. Editing is inline on the learner's own page, so there is no second layout to keep in step.
  • Study plan blocks carry articles — done 2026-09-10. Each block shows Articles with a reversible Mark as read, then Sessions with Study and Exam mode. Reading progress is per learner and separate from article_views: opening an article is not the claim that you finished it.
  • Admin settings page revamp — done 2026-09-11 by the Settings rebuild: People and AI models are native sections, AdminPage is deleted, and Site policy surfaced the registration switch that had no UI at all.
  • Image libraries — done 2026-09-10. Libraries, per-library grants, tags on the shared vocabulary, and MinIO behind a storage service.
  • Media management page (frontend) — done 2026-09-10. /media browses the bank by library, searches by what an image shows, and edits title, caption, alt text, tags and library in place; ids are on every thumbnail because that is what a question refers to, and an image with no caption is called out as one nobody will find. ImagePicker replaces the type-a-filename field on the question editor, and an image uploaded from it lands on the question directly.
  • Question folders — done 2026-09-12. See "Settings and access" above.

Article reading

  • Nested sections and per-section collapse — done 2026-09-10. A section may sit under an earlier top-level one (parent_id on the section JSON), the contents rail lists sub-sections under their parent, and an article opens as headings only, each expanding where it sits. Deep links open the target section and its parent.
  • Cross-references with previews — done 2026-09-10. [[slug]] or [[Label|slug]] in article prose becomes an in-app link that shows title, excerpt and section count on hover, from GET /articles/preview/{slug}. One fetch per article per page; no card on touch, where there is no hover.
  • Library browsed column by column — done 2026-09-10. The articles page is now the column browser itself: topics and the articles filed under them share a column, separated by icon. Search still answers with a flat list.
  • References — a section titled "References" is pinned last and styled. The numbered list with in-text superscript markers is deliberately NOT built: "with refernec, but you dont need in text reference". Closing this rather than leaving it open against an instruction.
  • Per-section notes and feedback — done 2026-09-12. Two things, kept in two tables on purpose: article_section_notes is private to the writer and nobody else ever reads it, article_feedback goes to whoever maintains the article and appears against the section on the article page. Both point at the section id inside articles.sections, not at article_section_index — those rows are dropped on unpublish, and a cascade from them would delete a learner's writing because an educator took an article down for an afternoon. A rename keeps a note attached (the id survives); a deleted section leaves the note marked orphaned, under the heading it was written on, for the writer alone to remove.
  • High-yield spans — done 2026-09-12, and half of it deliberately not done. Marking is built: ==like this== in the prose renders as a yellow <mark>, there is a Key button in the editor toolbar, and the AI drafter is asked to mark the high-yield view sparingly. The reader-facing show/ hide toggle is not built, against the instruction: "we dont need key points on or off like amboss". The High yield control is a single lit-or- not switch over the view; it no longer says "on"/"off" either.

Asked for on 2026-09-11 (evening), not yet done

  • Boxed quiz player — done 2026-09-11. A box the height of the window: the rail and the bottom bar stay put, the question scrolls in its own column, and the footer is out of the way while a session is being sat.

  • Share dialog — done 2026-09-11.

  • Sharing off site-wide — done 2026-09-11. Settings → Site policy. Stops new links; one already handed to somebody keeps working.

  • Remove per-question share/unshare — done 2026-09-12. Question.is_shared dropped; the bank is the bank and the grant tree decides who may manage a question.

  • Invite-only sign-up — done 2026-09-11. A switch plus single-use codes an administrator issues, with a note of who each is for and who it let in. The form asks for one only when the site needs it, and never says whether a code is valid before the account is made.

  • Settings, properly — done 2026-09-11: People and AI models rebuilt natively, AdminPage deleted.

  • Comments backend removed — done 2026-09-11: router, model and table dropped.

  • Image thumbnails — done 2026-09-12. ?w=256 and ?w=640 on /uploads/..., any other width refused with a 400 rather than honoured. EXIF rotate, never enlarged, WebP q82, derivatives under thumbs/{width}/{key} in the same bucket so credentials, lifecycle and backup are unchanged, and best effort throughout — a file that will not decode serves its original rather than failing. Pillow, not sharp. No Caddy caching: these are behind authentication and stay private, no-store; the win is the byte count. Asked for by the media grid, the figure strip's compact thumbnails, the figure manager and the quiz editor. Loose end: Pillow is not pinned in backend/requirements.txt. It arrives transitively via PyMuPDF and is 12.3.0 in the image today. Adding the pin invalidates the pip layer, and rebuilding that layer now fails outright because litellm==1.28.13 has been withdrawn from PyPI. Pinning Pillow therefore means re-pinning litellm first, which is a deliberate upgrade of the AI layer and not a side effect to slip into a thumbnail change.

  • Image thumbnails — done, and the note was stale: services/thumbnails.py makes 256px and 640px WebP derivatives with Pillow (not sharp — sharp is Node and this backend is Python), stores them beside the original under thumbs/{width}/{key} in the same bucket, and /uploads/...?w=256 serves them. Only those two widths exist, nothing is ever enlarged, and a derivative that cannot be made falls back to the original rather than failing the request. Every figure in prose now asks for the 256 and opens the original on a click. Still open, deliberately: HTTP caching. Uploads answer Cache-Control: private, no-store, so nothing is cached anywhere. A shared Caddy cache in front of access-controlled images would be a leak; what is safe is private, max-age=… on the derivatives, which is the browser's own cache and no one else's. And "make originals hard to pull" honestly means a signed short-lived URL per view — watermarking and right-click blocking are not that.

  • Image caching through Caddy — you mentioned a tool from the ped-ai work that generates thumbnails and caches through Caddy so images load fast, click to open full size, same bucket, and no straightforward download of the original. Not started: I need the name of that tool or a pointer to it before wiring anything, and "make originals hard to pull" is a decision about how far to go (a signed short-lived URL per view is the honest version; watermarking and right-click blocking are not). MediaAsset already stores one path per image, so a thumb_path beside it plus a Caddy cache rule is the shape.

  • QuestionEditModal is dead code — removed 2026-09-12, with its test. Nothing had imported it since Edit moved to the full page.

  • Per-question notes — done. The notes themselves were already built (question_notes, saved on blur); what was missing was removing the global notes tab that floated over the same screen, so it was never clear which notepad you were writing in. The global note stays on the dashboard.

  • Per-question feedback — done 2026-09-12. The learner's half already existed (the ⚑ item in the player's more-menu, question_feedback, the panel on the question editor, the header badge). What was wrong was the educator's end and the silence afterwards: reading, replying and deleting were gated on holding any grant rather than one over that question, so an educator given one branch could work through the whole bank; and the queue was narrowed by primary category alone, so a report on a question reached through an additional link, an exam or a folder grant belonged to nobody. Both now go through question_scope_predicate. A learner sees their own reports and any reply against the question they reported.

  • Tutorial mode — done 2026-09-12. Three coach marks on the overview, once per browser: the prepared session, search versus AI Mode, and what the ring is measuring. Steps are declared against a data-tour attribute rather than a class, and one whose target is not on the page is dropped from the count rather than shown pointing at nothing — an educator and a learner do not see the same dashboard, and "Step 2 of 6" must not promise a step that will never come.

  • Per-question performance table — done 2026-09-11 on the session analysis: position, excerpt, difficulty, time, peer percentage; sortable by position / slowest / hardest, ten a page.

  • Session analysis — done 2026-09-11. Sessions and analysis are one page: /sessions is the overall picture, /sessions/:attemptId one session, the sidebar lists every session. Recommendations grouped by Articles / Disciplines / Systems remain open (see Reading and study).

Dashboard

  • Overview page for signed-in users — done 2026-09-12. The dashboard is now the overview: greeting, a search hero whose Search and AI Mode controls open the one Ctrl+K panel on the tab they name (a second search box would be a worse copy of the panel's history, typeahead and hand-off into a conversation), the prepared session, "Continue your study", and a donut. One column on a phone, two from 1000px. The donut is a glance, not a page — the ring, its key and one line of basis, on the latest-attempt reading, with "See full analysis" beside the heading. The trend, the peer comparison and the focus-area table stay on /sessions, where they are worth the room. A new account is the good case, not the degraded one — one request to /study-tools/answer-split decides. With answers behind it: the ring, and MyNote under it. With none: no ring, no notebook, no dashes — the cold-start prepared session (which already plans an even spread across the blueprint and says so) plus three doors to reading, study plans and AI Mode. A failed request shows neither rather than calling an established account new. The old dashboard fetched /attempts/stats/dashboard and /attempts/history on every visit and rendered neither; both are gone. Still on the signed-out landing page as a separate job.

Analysis, as specified 2026-09-11 (evening)

Sessions is just sessions. Every general measure of performance lives on Analysis, which has three tabs.

Performance

  • Readiness — done 2026-09-12. GET /study-tools/readiness. Your score is the most recent answer to each question, and it is not called an equated score: we have no psychometrics to equate one. The peer card compares you with other learners on the questions you have in common, not with their scores on whatever they happened to sit — sitting the hardest fifty should not read as weakness. Both stay dashes until they mean something and say which of the two things they are waiting for.
  • Next step: adaptive session — already sat above the tab switch, so it shows on both. The readiness-locked note did too, and has been moved down to the table it is actually about.
  • Performance over time — done 2026-09-12. The running score across everything answered up to each day, not the session on its own. Locked below 40 answers or 3 sessions, saying which.
  • Analysis panel beside it — done 2026-09-12. GET /study-tools/answer-split counts the same answers twice: every answer ever given, and the most recent answer to each question. The donut is shared with the session view (components/Donut) and splits correct / correct after a tip / incorrect / unanswered.
  • Completion — done 2026-09-12. GET /study-tools/completion?days=N over 7 / 30 / 90 days or all time. Repetitions, course quizzes and expired attempts are left out; a blank is not a wrong answer.

Recommendations

  • Your knowledge profile — topics ranked by score, lowest marked FOCUS AREA, under three tabs: Articles, Systems, Disciplines. Columns: Topic, score, Relevance, Status, Action. A row expands to questions completed with a bar, answered-correctly with a correct/hints/incorrect bar, and a Start Qbank button for that topic. — done 2026-09-12 — paginated, with the three axes and expandable rows.
  • Relevance is the ABP content specification weight. This is the part we can do properly and AMBOSS cannot explain: a topic's relevance is the share of the real paper its domain accounts for, which exam_blueprints.weight already holds. Cardiology at 5% and rheumatology at 2% are not equally worth an hour, and the page should say so from the board's own numbers rather than from a guess.

Session Analysis

— done — exam_blueprints.weight drives the Relevance column and now adaptive selection too.

  • A rail of latest sessions — done. AnalysisShell owns the rail and the session list for both views. The session's recommendations now carry the Articles / Disciplines / Systems switch too, from GET /attempts/{id}/recommendations — and the membership rules (ancestor roll-up, article by category, system by symptom keyword) moved into services/knowledge_groups.py so the lifetime view and one session cannot disagree about where a question belongs.
  • Remove the overall figures from the session view. The four figures there are the session's own. The old ungrouped recommendations key is gone from the analysis payload with them.

Not us

  • No AI Mode for clinical content — a non-goal rather than a task. PedsHub is for learning; the clinical side is ped-ai's, and this product should not grow a second answer to a question that one already answers.
  • Move the PDFs into Tools. Upload, the document list and extraction all move off their own pages and onto the Tools page, because they are one job and were three places. — done — upload, the document list and extraction all live on /tools.
  • Nextcloud is the administrator's alone. Nobody else connects an account; it is an import path for whoever loads the corpus, not a per-learner integration. It moves out of everyone's Settings. — done.
  • Review and promote, on the same page. A run lands as a batch of drafts — already built, draft_batches / draft_questions, with their own sequence so nothing takes a question id early. What is missing is the screen: read them, fix them, reject the rubbish, then select the ones worth keeping and move them into the bank. Acceptance is the only moment a Question is created.

Collections, as shown 2026-09-11 (evening)

— done — a batch is read and accepted in place, and the question id is taken only on promotion.

  • A collections page — done 2026-09-12. /collections, in the section bar beside Qbank. Card and Table views (the choice is remembered), sort by last used / created / name / size with a direction control, a count line, and a search over name and date. Favorites leads as a fixed row. A shelf opens in place rather than linking away: there is no browsable question list to send anyone to, and a link that goes nowhere is worse than no link. Questions can be taken back out from there, and any shelf can be sat as a session. user_collections.last_used_at is new — opening or adding to a library counts as using it.

Search and AI Mode, as shown 2026-09-11 (evening)

Two screenshots, one flow.

  • The search overlay — done 2026-09-12. A panel that opens over whatever you are on, with a Search / AI Mode pair of tabs at the top — the same box asks the corpus or asks the model, and which one is a toggle rather than two separate destinations. Below the field: SEARCH HISTORY, the previous queries, and the keys spelled out — Ctrl+K open, ↑↓ navigate suggestions, Space use a suggestion, Enter submit. Opens from anywhere with Ctrl+K.
  • The overlay in AI Mode — done 2026-09-12. Switching the toggle to AI Mode changes what the same field does: the history and the key hints go, a mode picker appears beside the tabs ("Learning ⌄" — what the assistant is being asked to be), a clear button appears in the field once there is text, and the submit arrow fills in. The panel's own title tracks the chosen mode.
  • Submitting from AI Mode lands in the conversation — done 2026-09-12, as /ai?ask=. The overlay is only where the question is typed: pressing submit goes to the AI Mode chat with that question already asked and being answered — not to a results list, and not back to an empty box.
  • AI Mode as a page — done: conversation rail, citations at the end, practise from an answer.
  • Systems subsystems — done 2026-09-10. 69 subsystems created, 305 conditions nested; the tree is now System → Subsystem → Condition.
  • Cross-system duplicate conditions merged — done 2026-09-10. 216 names folded to one home each (347 rows removed, 1075 → 786), keeping the other systems as question_category_links rather than duplicate rows.
  • 99 of the 114 duplicated conditions merged — done 2026-09-11. The note above was wrong: 114 was the count of duplicated names, and 99 of them had a clear plurality. Merged with the same script and the same safety property as the earlier pass — each moved question keeps a question_category_links row to the system it came from, so no filter narrows. 786 categories to 662; 0 questions unfiled, 0 orphan links.
  • The 15 genuine ties are gone — closed 2026-09-12 as stale, not decided. The taxonomy rebuild gave every condition one home, so there is no longer a duplicate to merge: merge_duplicate_conditions reports 0 duplicated names and 0 needing a human call, and each of the 15 names in docs/tied-conditions.md now resolves to exactly one row (Chlamydial Infection became Chlamydia Infection under Adolescent Medicine). The doc is kept as the record of a decision that no longer needs making.
  • Primary categories pushed down — done 2026-09-10. 1,244 questions promoted from system to condition; 2,730 → 1,486 filed at system level. The system stays as a cross-link so no filter narrows.
  • Unfiled questions classified — done 2026-09-10. 1,170 of 1,486 filed by retrieval + a chooser constrained to the shortlist. 316 remain on a bare system: 301 the chooser declined and 15 with no viable candidate.
  • Every question filed below its system — done 2026-09-10. The last 316 were forced onto their nearest candidate; 0 remain on a bare system.
  • Review the classifier's work — moot 2026-09-12. The classifier assigned an organ system per question; systems now hang off the topic instead, and question_tag_links holds zero rows of type system. Every one of the 569 topics carries a system and all 2,922 questions reach one through their topic, so there is no longer a per-question pick to review.
  • The tied conditions are settled — done 2026-09-12. The rebuild took the tie count from 31 to 3, which is few enough to read. None was the even contest the counts implied: Q1201 teaches brain death determination rather than the intracranial hemorrhage that caused it, Q853 is gastrointestinal bleeding with juvenile polyp as a distractor, and Q826 was tagged Nursemaid's Elbow and Radial Head Subluxation — two names for one condition, and neither of them what a question about acute flaccid paralysis and MRI of the spine is about. scripts/settle_tied_primaries.py files each under one topic, cross-links the second and the old system, and drops the two wrong elbow links. promote_primary_categories now reports 0 ambiguous. Nursemaid's Elbow and Radial Head Subluxation are still two topic rows for one idea and want merging.
  • Real organ systems, and symptoms grouped under them — done 2026-09-10. The 27 "systems" were disciplines; that tree is now the Topics facet, and Systems is a new flat axis of 16 organ systems. question_tags gained parent_id/sort_order (migration e3f4a5b6c7d8), and 726 symptoms are filed under the system they present in. 3,536 stay top-level rather than being forced into an approximate bucket.
  • Taxonomy editing screen — done 2026-09-10. /categories is now Taxonomy: five tabs (Topics, Systems, Symptoms, Diseases, Subjects), each with create / rename / reparent / delete-with-move, and "+ Questions" to attach questions from a bank search. Everything added lands in the question bank and quiz builder immediately.
  • Article section index backfilled — done 2026-09-10. The index was empty because _rebuild_section_index only runs on save, so articles written before it existed had no rows. 10 articles / 28 sections indexed and embedded; section-scoped question links already worked.
  • Grant dimensions — done 2026-09-10. A grant names any combination of exam, discipline and category, and covers what matches all of them.
  • Exam-scoped disciplines — done 2026-09-10. A tag may be shared (exam_id NULL, e.g. Cardiology) or scoped to one exam. Boards Pediatrics and a future Step 1 Pediatrics are separate rows over different content, rather than one label spanning both. Pediatrics is bound to Pediatrics Boards and now covers all 2,948 questions. Facet counts are computed within the active exam and empty facets are hidden.

AI Mode design

Retrieval decides what the model may cite; the model only writes prose.

  1. Embed the learner's message, search every corpus (hybrid_ids already covers questions, articles, sections, cards, media).
  2. Put the retrieved rows in the prompt as the only permitted sources, each with its kind and id.
  3. The model cites by id from that list — [[article:7#features]] — never a URL.
  4. The server rewrites citations to links and drops any id that was not retrieved. A citation the model invented cannot survive.

That last step is the safety property, and it is enforced by the system rather than by the model behaving well — the same discipline as the article page no longer printing answers.

Settled: two kinds of link, handled differently.

Curated links already exist as tables — QuestionArticleLink, FlashcardArticleLink, FlashcardQuestionLink, LabReferenceCardLink — and carry a section id. An educator asserted them, so they are trusted and stored.

Retrieved links are computed per query by hybrid_ids and are not stored at all. They are ranked guesses.

The AI uses both: retrieval finds candidates, and a candidate carrying a curated link to another candidate is boosted, because an educator has already tied those two together. Nothing new is stored for citations. The only new tables AI Mode needs are conversations and messages.


From the 12 Sep screenshots — the exam player and what surrounds it

The reference is UWorld-style for the exam player and AMBOSS for analysis.

Exam player chrome

  • Top bar — done 2026-09-12.: Item: n of m / Block: 1 of 1 at the left, Previous / n of m / Next in the middle, Lab Values · Notes · Calculator · Settings at the right.
  • Bottom bar — done 2026-09-12.: Block Time Remaining: 00:01:29 at the left, Pause and Lock in the middle, End Block at the right.
  • One button, not two. — done 2026-09-12. A question with no answer offers Skip and Next today. It is one boxed control: one button.
  • Proceed to Next Item on the last question ends the block — done 2026-09-12 — End Block replaced three controls under two names. rather than doing nothing.
  • Cross out an option — already there — the ab control per option. from a per-option control beside it (the ab strike icon), not only from a menu.
  • Pause says "Exam Paused" and nothing else — done 2026-09-12. — a title and a Return to exam button. No warning about real exams; that is AMBOSS's disclaimer, not ours.
  • End Session is a plain confirmation — done 2026-09-12. — "Are you sure you want to end this session?", End Session / Cancel.
  • Right-click removes a highlight — done 2026-09-12 — the yellow is a solid band now, not a gradient stripe., and the yellow itself needs fixing.
  • The rail shows numbers while sitting an exam and shortened stems in review. — done 2026-09-12. It is numbers in both today.

Player review mode — specified 12 Sep from a finished exam

Reviewing a block that has ended looks like study mode, whether or not anything was answered. The screenshot is an exam that ran out of time with 0/2 answered, and it shows: the REVIEW badge, the rail with shortened stems, the correct option marked green with its explanation open beneath it, every other option struck red with the peer percentage beside it, KEY INFO / ATTENDING TIP / LABS, SHOW ALL EXPLANATIONS and HIDE STATS along the foot, and PREVIOUS / SKIP to move between them.

  • "Finished" means the attempt is closed — done 2026-09-12., not that every question was answered. The flag in QuizPage is answeredCount >= totalCount, which is wrong for exactly this case: a block that timed out with nothing answered is over, and it still shows numbers and hides explanations.
  • The player opens a completed attempt in review — done 2026-09-12. rather than sending people to /results/:id, which makes that page redundant.

Analysis

  • Session Analysis is the third tab — done 2026-09-12., inside the Analysis page with the session rail beside it — not a link away. Four figures, the donut, and Study recommendations under Articles / Disciplines / Systems.
  • An unfinished session shows Resume — done 2026-09-12., with the figures reading -- rather than a blank card.
  • Knowledge profile paginates — done 2026-09-12. (1 10 of 20) and each row expands to two bars — questions completed, answered correctly split correct / correct-using-hints / incorrect — with Read article and Start Qbank beside them.

Elsewhere

  • Cap replaces Turnstile, everywhere Turnstile was wired — one shared Captcha component, one captcha service, keys blank until issued.
  • The session rail scrolls — already worked — measured in headless Chrome at three viewports rather than assumed. once it holds more than a screenful.

Done this session

Hybrid search (full text + BGE-M3, RRF-fused) · embedding provenance and retry job · articles, cards, sections and media as searchable corpora · exams as real data with a per-user active exam · AI-mode matching from description or upload · category management page · tag vocabulary sanitised · question manager with bulk editing · per-category educator grants · full-page question editor · session rail with gradual reveal · articles read as one page · practise-this-topic · continue-study panel · PREP study plans · question version history · exam-scoped disciplines · tips in the prose ({{phrase|tip}}, underlined, opening where the phrase is) and the hint slice on the donut that goes with them.

From the 11 Sep recordings — still open

  • Quiz player, "let it not be showing when you're taking the quiz" — the recording does not say what "it" is. Most likely reading: in exam mode the right-hand question rail must not reveal right/wrong as you go (study mode may). Confirm before changing.
  • Study plan block counts — done. "1/1 article · 50/50 questions" beside each block, counts rather than a percentage, with the state alongside.
  • ArticleSplitView flake — fixed. It hovered a link, which starts a 350ms timer before the preview card appears; it now focuses instead, which reveals at once. The failures were it losing CPU to the backend suite.
  • Third analysis tab "Session analysis" — AMBOSS lists sessions as a tab; ours are the sidebar, which is what was asked for. Leave unless it comes up.