# AI-Examiner — Build Progress

Last updated: 22 October 2026 (sprint 1, part 2)

## Module status

| Module | Status | Notes |
|---|---|---|
| Schema (§3, PostgreSQL) | ✅ Done | `db/migrations/0001_*.sql`, `0002_*.sql`; applied by `db/migrate.py` and the Laravel bridge migration |
| M0 worker skeleton | ✅ Done | Job queue (`SKIP LOCKED`, dedupe keys, backoff, stale-lock reaper), `LISTEN/NOTIFY` wake-up, runtime config, audit/flags/usage |
| M0 web (Laravel, SSO, tenancy UI) | ⏳ Not started | Needs a PHP/Composer environment; schema bridge migration ready |
| M1 Question sets / structuring | ⏳ Not started | `labels.generate_alt_labels` ready for the editor |
| M2 Upload (web) | ⏳ Not started | |
| M2 Rasterize (worker) | ✅ Done | PDF/image → 200 DPI JPEG + AI copy; paper-relative blank detection |
| M3 Extraction | ✅ Done (fake provider) | Single pass, schema-validated, flags, cover-roll check, failover. **Not yet run against a real model** |
| M4 Assembly | ✅ Done | Deterministic; AI assist for ambiguous scripts not yet wired (§M4 step 8) |
| M5 Scoring + rescore | ✅ Done (fake provider) | One call per script; split by question when large; marks computed in code |
| M6 PDF | ✅ Done | PyMuPDF (no Chromium): cover page + annotated pages, line-anchored marks, margin layout with overflow handling, font subsetting |
| M7 Review UI | ⏳ Not started | Override arithmetic available in `scoring_math.validate_override` (port to PHP) |
| M8 Credits/analytics/exports | ⏳ Not started | Worker writes `usage_records`; ledger conversion belongs to Laravel |
| M9 Biddaloy integration | ⏳ Not started | |
| M10 Deploy/ops | 🟡 Partial | Worker Dockerfile, dev compose, CI workflow |
| M11 Evaluation harness | ⏳ Not started | Highest-priority next item together with real-model runs |

Tests: 66 passing (`cd worker && python -m pytest -q`), covering scoring arithmetic, label parsing, blank detection,
assembly scenarios, the full rasterize→score→PDF pipeline, margin overflow, rescoring with teacher overrides, retries, failover,
stale-lock recovery and tenant isolation.

## Decisions and deviations from design v1.1

1. **Schema is plain SQL in `db/migrations/`**, not Laravel PHP migrations. One source of truth that both Python
   and PHP apply (Laravel via `app/database/migrations/..._run_shared_sql_migrations.php`). New changes = new numbered file.
2. **psycopg 3 directly instead of SQLAlchemy** in the worker. The worker's queries are few and explicit; an ORM added
   nothing. Revisit only if query count grows a lot.
3. **Blank-page detection changed** (replaces design §8.4). The ink-ratio threshold missed a page with one short
   faint line (e.g. "= 5 cm" continuing a calculation): ratio 0.00006 vs threshold 0.0015, so the page would never
   have been read. New method: ink = pixels ≥ 35 grey levels darker than the page's own paper tone (90th percentile),
   ruled lines removed, only connected strokes ≥ 30 px counted; blank if < 120 stroke px. Test results: one light
   short word = 296 px (kept), grey paper with 300 dust specks = 0 px (blank), show-through = 0 px (blank).
4. **Jobs have a `dedupe_key`** with a partial unique index on live jobs. Combined with a row lock on the script,
   this makes "the last extracted page starts assembly" safe when pages finish concurrently.
5. **Review-required mode holds a script only when it has open warn/block flags** (spec EX-15), not every script.
6. **A `fake` provider** is the default in config so the full pipeline runs locally and in CI without API keys.
7. **Anthropic adapter does not send `temperature`**: the installed SDK (2.1.0) no longer exposes it on
   `messages.create`. Structured output uses forced tool use. OpenAI and Google adapters use JSON-schema output.
8. **Grapheme counting** (`regex` `\X`) is used for the 150-character brief and 60-character remarks, so Bengali
   conjuncts count as one character as SC-14 requires.

9. **M6 uses PyMuPDF instead of headless Chromium** (design §M6 alternative). Verified visually: Bengali conjuncts
   (ক্ষ, জ্ঞ, ন্ত্র, ঙ্ক্ষ, শ্রী, কৃষ্ণ, reph), Arabic RTL with harakat, ½ ¼ ¾, ✓ ✗ all render correctly. MuPDF does not
   fall back per glyph across fonts, so `textrun.spans()` wraps each Bengali/Arabic/Latin run in its own font.
   No browser in the worker image; marks are vector shapes over the original page JPEGs.
10. **Fonts must be subset** before saving: each text box embeds the full Noto fonts (14 copies of Bengali), which made
   a 2-page sample 9.2 MB. `subset_fonts()` brings it to 0.36 MB (PDF-08).
11. **Margin overflow**: remarks stack top-down without overlap; if a page has too many, text shrinks to a 7 pt floor,
   then spelling/grammar remarks fold into one "আরও Nটি ভুল" note. Score badges and explanations are never dropped;
   the underlines on the page still show every error.
12. **Spelling/grammar errors are underlined per line** (wavy) with "as written → correction" in the margin, because
   extraction gives line-level boxes only (spec v2.1 EX-12). Factual/method/calculation errors get a bracket + ✗.

## Known gaps / risks found while building

- **PDF text layer**: shaped Bengali does not map back to Unicode when text is copied out of the PDF (common with
  Indic scripts in PDF engines). Printed and on-screen appearance is correct; copy/search of Bengali inside the PDF
  is not. Data remains searchable in the database. If needed later: add an invisible text layer per line.

- Real-model behaviour (verbatim fidelity, bbox quality, heading separation) is untested until M11 runs on real
  scripts. The extraction prompt asks for headings as separate elements; assembly also detects labels inside lines
  as a fallback.
- Assembly cannot know if a repeated heading is a continuation unless the student writes a continuation word;
  otherwise it is treated as a duplicate attempt and flagged for the teacher.
- Provider adapters for OpenAI and Google are written against current SDK signatures but not exercised against
  live APIs.

## Next steps (in order)

1. **M11 + real models**: collect 50–100 real pages, run extraction with each provider, measure CER/digit accuracy/
   cost; choose models, image size and confirm bbox quality. This decides M6's placement strategy.
2. Tune M6 placement on real scans (tick/✗ position at true line ends; underline widths) once real bboxes exist.
3. **Web track M0/M1/M2** in a PHP environment: Laravel skeleton, SSO, question-set editor, upload.
4. M7 review UI, M8 credits/exports, M9 Biddaloy endpoints, M10 production deploy.
