Files
step3/QBANK_PDF_IMPORT_RUNBOOK.md
T
2026-07-13 22:32:31 -05:00

348 lines
14 KiB
Markdown

# Question Bank PDF Import Runbook
This runbook describes how to turn an image-based board-review PDF into
original Step 3-style questions, verify them, insert them through the Questions
API, and confirm the production result. It records the workflow used for the
July 2026 NBME 5 import and strengthens it with the repository's current
reference, originality, image, and duplicate-review requirements.
The source material is used only to identify tested concepts, reasoning paths,
and plausible distractors. Do not copy source wording, vignette structure,
distinctive fact combinations, answer wording, or images.
## July 2026 Result
- 221 image-only PDF pages were rendered and OCRed.
- Page markers were mapped to 176 source questions across five sections.
- Questions were generated in batches of four.
- The first 72 questions passed the initial hosted-model workflow.
- Of the remaining 104, an independent audit approved 95 and rejected 9.
- The 9 rejected questions were regenerated from scratch with the audit issues
supplied, then all 9 passed a second independent audit.
- The bulk API inserted 104 remaining records, bringing the imported set to
176 records.
- Direct database verification found 0 missing external IDs and 0 answer-key
mismatches.
Known gap in that historical run: model-based medical review was completed, but
authoritative references were not embedded in the `nbme5-*` question documents.
Future runs must complete the reference-review gate below before setting
`isPublished` to `true`. The historical records should be treated as awaiting
that separate reference review.
## Prerequisites
- Python 3 with `PyMuPDF`, `Pillow`, and `pytesseract`.
- A local Tesseract OCR installation available on `PATH`.
- PowerShell, `curl`, the .NET SDK, and access to this repository.
- Access to a capable generation model and a separate audit model.
- A `SUPER` JWT for the target Questions API.
- Optional direct MongoDB read access for final production verification.
Keep secrets out of scripts, prompts, logs, and the repository. Configure them
in the current process:
```powershell
$env:QUESTIONS_API_BASE = "https://example.invalid"
$env:QUESTIONS_SUPER_JWT = "<short-lived SUPER token>"
$env:QUESTIONS_MONGODB = "<optional read-only connection string>"
```
Create all intermediate files outside the repository:
```powershell
$RunId = Get-Date -Format "yyyyMMdd-HHmmss"
$WorkDir = Join-Path $env:TEMP "step3-qbank-import-$RunId"
New-Item -ItemType Directory -Path $WorkDir | Out-Null
"ocr", "mapping", "prompts", "generated", "audits", "repairs" |
ForEach-Object { New-Item -ItemType Directory -Path (Join-Path $WorkDir $_) | Out-Null }
```
Suggested layout:
```text
step3-qbank-import-<run-id>/
ocr/pages.jsonl
mapping/items.jsonl
prompts/
generated/
audits/
repairs/
validation.json
references.json
insert-payload.json
```
## 1. Establish The Import Set
1. Hash and record the source PDF before processing.
2. Record the expected source, section, and question counts if known.
3. Query existing external IDs before generation. Never assume the database is
empty, and never overwrite an existing ID unless that update is intentional.
4. Choose a deterministic ID prefix, for example
`nbme5-s3-i007-<concept-slug>`.
5. Keep the original PDF unchanged and outside generated-output directories.
The bulk endpoint upserts by `externalId`. Deterministic IDs make retries
idempotent, but they also make accidental overwrites possible. Compare the
planned IDs with production before insertion.
## 2. Render And OCR The PDF
For an image-only PDF, render each page at approximately 250-300 DPI and run
Tesseract on the rendered bitmap. A minimal Python outline is:
```powershell
py -m pip install pymupdf pillow pytesseract
```
```python
import io, json, sys, fitz, pytesseract
from PIL import Image
source_pdf = sys.argv[1]
pages_jsonl = sys.argv[2]
document = fitz.open(source_pdf)
with open(pages_jsonl, "w", encoding="utf-8") as output:
for page_number, page in enumerate(document, start=1):
pixmap = page.get_pixmap(matrix=fitz.Matrix(3, 3), alpha=False)
image = Image.open(io.BytesIO(pixmap.tobytes("png")))
text = pytesseract.image_to_string(image, config="--psm 6")
output.write(json.dumps({
"page": page_number,
"text": text,
}, ensure_ascii=False) + "\n")
```
Save the snippet as `ocr_pdf.py` in the working directory and run:
```powershell
py "$WorkDir\ocr_pdf.py" "C:\path\source.pdf" "$WorkDir\ocr\pages.jsonl"
```
Do not silently discard low-confidence or empty pages. Save page numbers and OCR
text so every generated question remains traceable to its source pages during
review.
## 3. Map Pages To Source Questions
Parse repeated headers such as `Exam Section N: Item M of K`. Group all pages
for the same section/item pair and append continuation pages to the preceding
item when necessary.
Each `items.jsonl` record should contain:
```json
{"section":3,"item":7,"pages":[95],"sourceText":"..."}
```
Before generation, verify:
- every detected section/item pair is unique;
- the item sequence has no unexplained gaps;
- multipage stems and answer choices are complete;
- the total mapped-question count matches a manual review;
- OCR did not convert option letters, decimal points, inequality signs, units,
or answer-choice text into unusable content;
- image-dependent questions are identified for special handling.
The July 2026 source had 221 pages but only 176 questions. Page count is not a
question-count proxy.
## 4. Generate Original Questions
Generate small batches, typically four source items per request. Small batches
limit loss when a model call fails and make outputs easier to review.
The generation prompt must require:
- a substantially original clinical scenario testing only the same concept;
- changes to age, sex when clinically possible, setting, chronology, numeric
values, sentence sequence, and answer wording;
- one defensible best answer and a variable pool of plausible choices in an
array of `{id, text}` objects, not a map keyed by letters;
- a stem containing only the vignette and a separate `leadIn`;
- `choiceExplanations` with exactly one entry for every main choice;
- `wrongAnswerFollowups` for every incorrect main answer;
- three A-C follow-up choices, all of which restate true facts explicitly found
in the new stem or displayed labs;
- Celsius and Fahrenheit together for every temperature;
- labs only when useful and only when their values also appear in the stem;
- `media: []` unless separately reviewed, licensed media will be added later;
- a `references` array that the reference-review gate populates with complete
authoritative source records before publication;
- selection percentages for every authored choice initialized to zero;
- established Step categories, 3-6 tags, and a unique deterministic ID.
For image-dependent source items, rewrite the concept as a text-based question
when that can be done without changing the learning objective. Otherwise stop
and perform the image copyright workflow in `README.md`; do not import the
source image.
Run a four-question gate before scaling out. Inspect it manually for
originality, ambiguity, schema shape, medical accuracy, and stem-anchored
follow-ups. Reject the model or revise the prompt if this gate fails.
## 5. Run Deterministic Validation
Combine generated batches only after each parses as JSON. The validator should
fail the run for any of the following:
- missing, duplicate, or incorrectly prefixed external IDs;
- a count different from the mapped source count;
- fewer than five main choices, more than twenty, duplicate choice IDs, or a
main choice not represented as an `{id, text}` object;
- an invalid `correctChoiceId`;
- missing choice explanations or a correct explanation that does not begin
with `Correct.`;
- follow-up keys that do not exactly equal the incorrect main-answer IDs;
- follow-up choices not represented by an ordered A-C array;
- follow-up text that cannot be traced to the new stem or labs;
- nonzero initial selection percentages;
- invalid difficulty, category, tag count, publication state, or media shape;
- a Celsius temperature without its Fahrenheit equivalent;
- labs referenced by explanations or follow-ups but absent from the stem;
- suspiciously long contiguous overlap with source wording.
Treat automated overlap and anchoring warnings as review queues, not automatic
approval or rejection. Record the disposition of every warning.
## 6. Perform Independent Content Review
Use a different model or reviewer from the generator. Provide the OCR source
and generated question, and request a machine-readable report:
```json
{"externalId":"...","approved":false,"issues":["concrete issue"]}
```
The reviewer must check:
1. The new item tests the same core reasoning path.
2. It is structurally and textually original.
3. The keyed answer is correct under current practice.
4. No distractor is reasonably co-correct.
5. All explanations are accurate.
6. Every follow-up choice is an explicit fact from the generated stem or labs.
7. The vignette is internally consistent and sufficient to answer.
Regenerate rejected items from scratch with the audit issues included. Do not
patch only the reported sentence when the underlying scenario is ambiguous.
Audit every repaired item again. Merge a repair only after it receives an
independent approval with an empty issue list.
## 7. Complete Required Reference And Duplicate Reviews
Model agreement is not authoritative medical verification. Before publication:
1. Verify each keyed answer and material explanatory claim against current,
direct, authoritative sources such as professional guidelines, government
agencies, official labeling, or primary research.
2. Add every verified source to the question document's `references` array,
including title, publisher, direct HTTPS URL, supported claim, ISO access
date, and reviewer. Do not maintain a separate repository reference ledger.
3. Search the entire question bank for exact and near-duplicate learning tasks,
not just duplicate text or IDs.
4. Record duplicate-review dispositions and substantially differentiate or
remove near-duplicates.
5. Complete the image licensing review for every non-empty `media` field.
Keep new questions unpublished until these gates are complete. If generation
produces `isPublished: true`, change it to `false` before insertion and publish
only after the reviews are recorded.
## 8. Build And Submit The Payload
The API expects this wrapper:
```json
{"questions":[{...},{...}]}
```
Submit the validated file exactly as UTF-8 bytes:
```powershell
curl.exe --fail-with-body `
-X POST "$env:QUESTIONS_API_BASE/api/questions/bulk" `
-H "Authorization: Bearer $env:QUESTIONS_SUPER_JWT" `
-H "Content-Type: application/json; charset=utf-8" `
--data-binary "@${WorkDir}\insert-payload.json"
```
Expect `{"upserted":N}` with HTTP 200. PowerShell 5.1 can transmit a string body
with an unexpected encoding; prefer `curl --data-binary` or pass explicit UTF-8
bytes to `Invoke-RestMethod`.
Do not insert partial batches merely because generation is expensive. Complete
the validation, review, repair, reference, and duplicate gates first.
## 9. Verify Production
Verification is complete only when all of these pass:
- the API reports the expected upsert count;
- the database contains every expected external ID exactly once;
- every stored `correctChoiceId` matches the final payload;
- every stored `references` array exactly matches the verified payload;
- a targeted deployed-API search returns at least one newly inserted question;
- the returned question has the expected choices, answer key, and publication
state;
- repository state is unchanged unless documentation or tooling was
intentionally committed.
Do not use an unfiltered search to prove completeness. The API limits search
results and randomizes the returned order. Compare IDs directly through a
read-only database query or a dedicated administrative verification tool, then
use a highly specific tag combination for the deployed-API read check.
The final verification report should include at least:
```text
EXPECTED=<count>
STORED=<count>
MISSING=<count>
DUPLICATES=<count>
ANSWER_MISMATCHES=<count>
API_READ_RESULTS=<count>
```
## 10. Retain Reviews, Then Clean Up
Retain authoritative references in each question document. Retain the
duplicate-review record, validation summary, audit decisions, and final
ID/answer-key manifest according to project policy. These are review records,
not disposable generation scratch files.
After production verification:
1. Delete rendered page images, raw model logs, rejected generations, temporary
prompts, local test projects, and disposable payload copies.
2. Preserve or securely dispose of the original PDF according to its license
and organizational policy.
3. Remove large local models installed only for the import.
4. Confirm the repository is clean with `git status --short --branch`.
5. Commit and push only maintained documentation, tooling, and review records.
Use explicit, workspace-contained paths for cleanup. Resolve each path before a
recursive deletion and never delete the repository or source PDF as part of a
generated-artifact cleanup.
## Acceptance Checklist
- [ ] Source PDF hash and import scope recorded.
- [ ] OCR page count and mapped-question count reconciled.
- [ ] Every source item mapped exactly once.
- [ ] Four-question generation gate passed.
- [ ] Full schema and ID validation passed with zero errors.
- [ ] All anchoring and source-overlap warnings reviewed.
- [ ] Independent content audit approved every question.
- [ ] Every rejected question regenerated and re-audited.
- [ ] Authoritative references embedded in every question document.
- [ ] Full-bank duplicate review completed.
- [ ] Image rights cleared for every media-bearing question.
- [ ] Final payload contains only intended new or intentionally updated IDs.
- [ ] Bulk API returned the expected upsert count.
- [ ] Database has zero missing IDs and answer-key mismatches.
- [ ] Deployed API read check passed.
- [ ] Required review records retained and scratch files removed.