| E-001 |
2026-09-30 |
Stack = FastAPI (Python 3.12+) backend + React/Vite/TS frontend, in-memory index built at deploy, no DB. |
Arabic NLP tooling is strongest in Python; the in-memory index needs a long-running process, not a serverless worker. |
ADR-001 |
| E-002 |
2026-09-30 |
Dual-orthography matching: loose normalization for retrieval, strict re-verification before found. |
loose-only normalization returns found for misspelled ayat (verified on 11 verses). |
ADR-002 |
| E-003 |
2026-09-30 |
Exact matching runs on the full inverted index, RRF only for fuzzy fallback. |
RRF top-100 can drop the exact hit. |
ADR-002 |
| E-004 |
2026-09-30 |
Vector channel OFF in P0 (RETRIEVAL_VECTORS=off). |
unmeasured recall, +RAM, +licence row. |
ADR-002 |
| E-005 |
2026-09-30 |
Quran states: found (strict 1.0) / needs_review (with diff + السورة:الآية + qirāʾa caveat, one render unit) / not_found (no candidates shown). partial_match never for Quran. |
Audit B-A1/A2 + SAFETY §1. |
ADR-003 |
| E-006 |
2026-09-30 |
Hadith partial_match carries a transmission-variant caveat. Cross-corpus rule: a 1.0 Quran match wins regardless of attribution. |
Audit B-A5/A8. |
ADR-003 |
| E-007 |
2026-09-30 |
No storage of user input, period. Report button → pre-filled GitHub Issue. No server-side report store. |
Terms §9; audit C2. |
ADR-004 |
| E-008 |
2026-09-30 |
Provider layer = LLMClient interface with MockProvider (offline, deterministic) as default in dev/test; real OpenAI-compatible providers are plugged in by configuration. |
Determinism and testability. |
ADR-005 |
| E-009 |
2026-09-30 |
messages/ar.json & en.json are the single source of user-facing prose; frontend imports them at build. Forbidden-word scanner whitelists corpus book names and literal corpus fields. |
Audit C1/C6. |
ADR-003 |
| E-010 |
2026-09-30 |
Display Quran in Uthmani rasm (Tanzil), index both rasms. |
The challenge's approved Quran source is the Uthmani text; display must match it. |
ADR-002 |
| E-011 |
2026-09-30 |
/health returns 503 until corpus loaded; reports doc counts, RSS, build sha. |
A health probe must not report ready before the index is loaded. |
ADR-001 |
| E-012 |
2026-09-30 |
Min quote length: 2 tokens Quran / 3 hadith unless quote-marked; ≤30 spans/request; positions shown ≤5 with total count. |
Bounds response size and keeps very short spans from producing coincidental matches. |
ADR-003 |
| E-013 |
2026-10-01 |
corpus/fetch.py TLS policy: on a certificate failure (expired/untrusted/hostname) and only for files with a pinned sha256, retry once without certificate verification, print a loud warning, then enforce the sha256 as usual. --strict-tls forbids the fallback. Unpinned sources never fall back. |
tanzil.net served an expired Let's Encrypt cert (notAfter 2026-09-30 11:47 UTC) on 2026-10-01, breaking bootstrap.sh step 1 on a fresh machine. The sha256 pin already makes content integrity independent of the channel; a hard failure would block all work for an upstream ops lapse. Downloaded bytes re-verified identical to the pinned hashes. |
ADR-001 (corpus pinning) |
| E-015 |
2026-10-01 |
Evidence tie order is deterministic and display-aware: on equal score → Quran, then a record whose text may be shown (OHD) before a link-only HadeethEnc record, then Sahihain before the other seven, then record index. Status is unaffected; only which record is rendered first. |
In HADEETHENC_MODE=link the top hadith hit would otherwise be an empty-body card with no visible diff. |
ADR-003 |
| E-016 |
2026-10-01 |
V1 extended for link mode: a HadeethEnc match must carry an EMPTY source_text when HADEETHENC_MODE=link; any text → validator reject. Grade/takhrij/link still shown verbatim. |
Enforces Q2 default mechanically. |
ADR-003/005 |
| E-017 |
2026-10-01 |
Test fixture index = filtered subset of the real records.jsonl (corpus/build_fixture.py), git-ignored, auto-built by conftest.py when the full index exists; tests skip otherwise. No synthetic religious text in tests. |
CI without 229 MB; identical record format exercises every code path. |
— |
| E-018 |
2026-10-01 |
Frontend UI strings: backend messages/*.json remain the single source for every result/notice/error string (imported at build); a separate small UI catalogue in frontend/src/i18n.ts holds chrome only (buttons, headings). Template variables are filled only from matcher/corpus fields. Source text is rendered byte-exact with <mark> wrappers at backend-supplied char ranges; no letter-spacing, no text-transform, no .normalize(. |
E-009 + research/06 rules 3–6. |
ADR-003 |
| E-020 |
2026-10-01 |
Image-sourced quotes are never found. Any quote whose source_modality=image that would be found is downgraded to needs_review / image_unconfirmed (message needs_review_image), matches still shown; the response carries ocr_text so the user verifies what was read. |
Observed live: gpt-5.4 OCR silently "corrected" «علي» → «على», which would have turned a misquote into a confirmed found. |
ADR-003 |
| E-021 |
2026-10-01 |
scripts/serve.sh uses a real provider when OPENAI_API_KEY and OPENAI_BASE_URL are set, otherwise the offline mocks. |
One command for local development either way. |
scripts/serve.sh |
| E-022 |
2026-10-01 |
Introducers are matched on loose tokens with honorific swallowing («صلى الله عليه وسلم», «عليه السلام», «رضي الله عنه», «تعالى»…), so tashkeel/variants after «قال رسول الله» never become the quote. |
OCR output «قال رسول الله صلَّى اللهُ عَلَيْهِ وسلَّمَ:» produced a bogus quote «صلى الله عليه وسلم» that matched Bukhari 1. |
— |
| E-023 |
2026-10-01 |
Canonical composition in the strict tier: a base letter + combining madda/hamza (ا+U+0653, ا+U+0654, ا+U+0655, و+U+0654, ي/ى+U+0654) is composed to the precomposed letter (آ أ إ ؤ ئ) before comparison. Loose tier unchanged. Index rebuilt (sha 54d34102…). |
Tanzil Uthmani writes «آ» as ا+U+0653 (2 044 ayat), every other text uses U+0622 → a byte-faithful «كَمَآ» was reported as an orthographic difference. Found by IslamicEval 1B analysis (B-Q26_2, B-Q45_7). |
ADR-002 |
| E-024 |
2026-10-01 |
Mixed-rasm tolerance. Each Quran position knows its twin's spelling in the other rasm (G2/GS2, filled only for the 5 873 ayat whose Uthmani and simple texts are word-aligned; the 363 misaligned ayat keep G2==G). The strict gate, the loose exact check (mixed_rasm_hit, only for fuzzy windows of exactly the quote's length) and the diff accept a word in EITHER rasm. Hadith unaffected. Eval mutations that would land on the twin spelling are skipped (shared ortho_foldable). |
Real quotes mix «آمنا» (simple) with «إسحق» (Uthmani) in one sentence (IslamicEval B-Q27_1, B-Q27_5, B-Q28_5); both are Mushaf text and must not be flagged. Verified: make eval-full 150/150, 0 unsafe, FA 0/500 after the change. |
ADR-002 |
| E-025 |
2026-10-01 |
Surah-stream fuzzy for multi-ayah quotes (fuzzy_search_surah_stream): around each per-ayah seed a ±n window of the surah stream is re-scored; hit attributed to the ayah where the window starts; continues_to + «الآيات a–b» label now also for fuzzy hits; diff/highlights computed on the display stream of the surah, chars outside the first ayah unmapped (source_chars=[-1,-1]). |
«… الْمَشْحُونِ * ثُمَّ أَغْرَقْنَا …» (26:119–120) scored 0.50 against any single ayah → not_found; now 0.90 → needs_review with the one differing word highlighted (IslamicEval B-Q24_1/2, B-Q40_1). |
ADR-002 |
| E-026 |
2026-10-01 |
quran_fragment notice: a found Quran quote that does not start at an ayah head or end at an ayah tail (in any position) carries the fixed notice «النص المنقول جزء من الآية لا الآية كاملة». Status stays found — a verbatim fragment is faithful text. |
IslamicEval guideline C1 counts an incomplete ayah as an error; we disagree on the status but surface the fact. |
SAFETY §1.3 |
| E-027 |
2026-10-01 |
IslamicEval 1B mapping is kind-aware: found in the corpus matching the claimed kind → Correct; found in the other corpus → Incorrect; everything else → Incorrect. partial_match is never Correct. Runner uses the product's own stages (_prepare → _gather_evidence → decide), no LLM. |
A verbatim ayah introduced as a hadith (B-Q24_3) is a wrong citation; the first mapping counted it Correct. |
eval/islamiceval |
| E-028 |
2026-10-01 |
Bare «قال»/«قوله» is an introducer only when «ﷺ» follows it in the raw text (the ligature is dropped by the normalizer, so «قال ﷺ» used to collapse to «قال»). |
IslamicEval 1A: «قال ابن عثيمين …», «قال: قيس بن الحارث» etc. became quotes → 54 false spans in 50 responses. |
E-022 |
| E-029 |
2026-10-01 |
Kind of a bracketed quote from context: an introducer immediately before the bracket, else a claimed source (surah:ayah → quran, hadith book → hadith_matn), else unknown. Status unaffected (I4 still wins). |
40 of 182 overlapped gold spans carried the wrong label only because "…" yields unknown → hadith scope. |
— |
| E-030 |
2026-10-01 |
Bracketed attributions are not quotes (_is_attribution): «(رواه مسلم)», «(متفق عليه)», «(سورة البقرة, آية 257)», «(البقرة: 255)», «(عليه الصلاة والسلام)», bare numbers. |
61 parenthesised references were checked and shown as «لم يوجد». |
— |
| E-031 |
2026-10-01 |
IslamicEval 1A runner (eval/islamiceval/run_1a.py, make islamiceval-1a): the product's real Pipeline.check(); label from match evidence; official scorer re-implemented (regex <Response> extraction — the XML is not well-formed; exclusive end; per-question char macro-F1); responses > 5 000 chars split at paragraph breaks (limit kept). Two configs: rules 62.96, rules+llm (gpt-5.4) 67.48 on dev. Published test: 90.06 / 87.20 / 86.11. |
Deck slide 10 promised the external benchmark; the honest number and its causes are more valuable than a tuned one. |
eval/islamiceval |
| E-032 |
2026-10-01 |
not_found is never red. Badge, border and icon for «لم يوجد في مصادرنا» use a neutral slate (--bs-state-notfound: #4b5280 light / lighter in dark), distinct from --bs-error. Red is reserved for transport/validation errors. |
SAFETY §1.3: «لم يوجد» is a statement about our sources, not a verdict on the text; the first brand kit coloured it like an error. |
SAFETY §1.3 |
| E-033 |
2026-10-01 |
Brand kit v1.0 adopted with corrections: kit tokens/icons/logo/fonts shipped unmodified under frontend/src/brand/ + public/brand/; overrides live only in index.css. Mushaf font = Amiri Quran (OFL) subset, not the kit's KFGQPC reference (not redistributable). share_target removed from the manifest (no data retention), example.com URLs dropped. Readex Pro is the only UI font (verified against the official competition pptx, 8 763 uses). |
Quality over cost. Every kit claim was re-verified (checksums, SVG validity, contrast ≥ 4.5:1, font licences) before use. Superseded visually by E-056. |
frontend/src/brand/, index.css |
| E-034 |
2026-10-01 |
E2E assertion that waits on a real provider gets an explicit 45 s budget; all other assertions keep Playwright's 5 s default. Inline footer links get a 24 px hit area (.source a). |
First real Chromium run: the only failures were a timing false-negative and a genuine WCAG 2.2 SC 2.5.8 miss on 8 licence links (18 px). Both fixed; axe 0 violations on light/dark × desktop/mobile. |
frontend/ |
| E-035 |
2026-10-01 |
LLM_REASONING_EFFORT pass-through (default none in scripts/serve.sh). Span location is copy-work; reasoning adds latency, not spans. |
Measured on the proxy, same text, same spans: gpt-5.4 default 8.7–14.9 s · low 4.5–6.9 s · none 2.0–2.8 s; gpt-5-mini minimal 1.3–1.5 s. Full /v1/check 8.0 s → 1.96 s; E2E 12.9 s → 6.7 s. Vocabulary is per model (gpt-5.4 rejects minimal, gpt-5-mini rejects none) → value forwarded verbatim, unsupported → 400 → rules-only, visibly (extraction_degraded). |
CAPABILITIES |
| E-036 |
2026-10-01 |
Topbar collapses below 400 px: health pill → dot (label visually hidden, still read by AT), language button → icon only. text-wrap: balance on h1, pretty on paragraphs. |
Pixel audit at 360 px: «English» button clipped 13 px off-screen (scrollWidth 373 > 360). Now 0 overflow at 320/360/390/412/768/1024/1440. |
frontend/ |
| E-037 |
2026-10-01 |
Decorative logo inside the brand link (LogoMark title="" → aria-hidden, no <title>); robots.txt shipped (disallow /v1/, /docs, /openapi.json). |
Lighthouse: label-content-name-mismatch (aria-label «بصيرة» ≠ visible «بصيرة Basira») and robots-txt 47 errors (SPA HTML served as robots.txt). Scores after: mobile 99/100/100/100, desktop 100/100/100/100. |
frontend/ |
| E-038 |
2026-10-01 |
Hosting floor = 2 vCPU / 4 GB RAM is sufficient for one uvicorn worker + the static frontend; the MCP server shares the same Pipeline instance (no second index load). |
Measured RSS ≈ 314 MB after snapshot boot. |
Dockerfile, mcp_server.py |
| E-039 |
2026-10-01 |
Precision and Quran-gate release: I10 not_found wording follows the claimed kind (not_found_any for unknown), I11 Quran-only diacritic gate (match/harakat.py), letter-level diff, «وقال/فقال» introducers, corpus-anchored seed-and-extend detector (extract/anchor.py), explicit-tag spans, isnad/claimed_source segments, IslamicEval 2026 runners. |
Both headline bugs reproduced on main before merge; all gates green after (129 tests, eval-full 150/150 FA 0/500, smoke OK); IslamicEval 2026 Task 2 Ayah 98.71 and Task 1 F1 0.6446 reproduced with the official scorer. 1B drops 89.47→89.07 on one span whose gold is wrong (B-Q23_7 «شِيْءٍ»). |
match/harakat.py, extract/anchor.py, eval/islamiceval/ |
| E-040 |
2026-10-01 |
Snapshot boot evaluated before adoption: 2.3 s / 249 MB with byte-identical results; the duplicate Quran scan was dropped in favour of the anchor detector; the CSP was corrected to allow the inline theme script by hash. |
Measured before merge; invariants renumbered I12/I13 so status-affecting invariants keep the lower numbers. |
snapshot.py |
| E-041 |
2026-10-01 |
Snapshot boot, security headers and scholar-lens notices merged: invariants I12 (claimed ayah ref mismatch → notice) / I13 (attribution cross-notice); a separate whole-text scan was dropped (the anchor detector is the single unmarked-quote path; its 3 tests were retargeted to it); partial_ayah_context folded into the existing position-aware quran_fragment (E-026) — one notice, not two; CSP script-src now carries a sha256 hash computed at startup from the built index.html for the theme pre-paint script (JSON-LD is data, excluded; test added); Dockerfile brand/ path fixed. Kept: binary snapshot boot (snapshot.py, v3 with GS2/G2), security headers, same-origin static serving, determinism_hash per response, repeated_in_text, qiraah_note, basmala_note, attribution_*, claimed_ayah_mismatch, EN introducers, noise filter, Docker/compose, CI. |
Measured after merge on the live server: boot 1.81 s from snapshot (24.2 s build), RSS 283 MB (was 738–1 101 MB); pytest 165/165; eval-full 150/150 · 0 unsafe · FA 0/500; smoke OK; vitest 8/8; E2E 1/1 (6.7 s). |
match/harakat.py, extract/anchor.py, eval/islamiceval/ |
| E-042 |
2026-10-01 |
Results workspace: highlights inside the user's own text (AnnotatedText, ResultsView). Every detected quote becomes a tinted/underlined <button> in place, coloured by state; isnad and claimed-source are dotted satellites; repeats dimmed. Desktop ≥960 px: text + sticky details panel with prev/next; mobile: tap → bottom sheet (role=dialog, Escape, focus restore, scroll lock). Status strip doubles as filter. Invariant: annotated.textContent === input (unit + E2E). Falls back to the card list if no span is placeable. |
Results were shown apart from the text the user wrote; the workspace keeps both together. Measured after: axe 0 on 5 contexts (light/dark × desktop/mobile/sheet/320), targets ≥24 px, overflow 0, Lighthouse mobile 98/100/100/100, desktop 100/100/100/100; vitest 13/13, E2E 2/2. |
frontend/src/components/ |
| E-043 |
2026-10-01 |
CLS root cause fixed: the health pill's label changes from «الخادم يحمّل المدوّنة…» to «جاهز» ~300 ms after mount and re-flowed the top bar (CLS 0.078 → 0). Fix = reserved label width per breakpoint (9.5em / 4em / hidden ≤400). A metric-matched Readex Pro Fallback (ascent-override 100%, descent-override 25%, size-adjust 104%, from fontTools) also added. Rejected attempt: scrollbar-gutter: stable made CLS worse (0.156) — reverted. |
Diagnosed with a PerformanceObserver('layout-shift') trace under Lighthouse-like throttling, not by guessing. |
frontend/ |
| D-013 |
2026-10-03 |
Harakat policy (B01). Any vowel/shadda/sukun the user WROTE that contradicts the Mushaf on the same letter is a difference — including the last letter of a word («يَخْشَى اللَّهُ» vs «ٱللَّهَ» 35:28). Marks the user did not write are never a contradiction, but when the quote carries any mark the letters whose marks are absent are highlighted (harakat_incomplete, status unchanged). Single exception: a sukun on the LAST letter of the whole quote where the Mushaf has a vowel = pausal form → found + waqf_note. |
Policy: any vowel difference inside an ayah is always shown; the only exception is a sukun on the final letter. A test case proved the old "never judge the final letter" rule passed a meaning-flipping change. |
match/harakat.py::compare_words (conflict / missing / waqf); pipeline._harakat_gate; test_safety_gates.py (15). |
| D-014 |
2026-10-03 |
Vocalised reference = Tanzil «simple» (fully vocalised, common orthography), pinned in manifest.json as tanzil_simple (sha256 f3268cfe…344b), carried as Record.text_vocalized; harakat ONLY — never displayed, never indexed. Falls back per word to the Uthmani display text when the user's spelling aligns only with it («مَلِكِ»). |
Verified 6236/6236 ayat token-identical (loose+strict) with simple-clean, so user marks align letter-for-letter where Uthmani small/omitted letters would not. qurancomplex.gov.sa/quran-dev was unreachable at the time (timeout); revisit when reachable. |
corpus/build_index.py (tv), store.py, snapshot v4. |
| D-015 |
2026-10-06 |
Rasm-uniqueness proof (I18/I19). A quote typed without hamza / ى / ة whose only differences from the record are unwritten marks is found (notice rasm_completed, completed letters listed) iff the corpus spells that span exactly one way across every position; otherwise, or when the user wrote a different mark (علي for على, أن for إن), needs_review/rasm_ambiguous with the competing spellings and their counts. |
People type bare Arabic (quran.com, Dorar, sunnah.com all fold silently). Measured on Tanzil Hafs: 87.7 % of token occurrences have one Mushaf spelling for their bare form; the 12.3 % that do not are exactly the إن/أن · على/علي · إلا/ألا family. The proof is read from the corpus, never guessed; V7 re-derives it. Harakat policy (D-013) untouched. |
match/rasm.py, state.py, verify.py (V7), schemas.RasmInfo, eval/materialize.rasm_expectation, tests/test_rasm.py (25) |
| E-044 |
2026-10-03 |
Foreign material gate (B02): Latin letters / digits / other scripts BETWEEN the quote's words («قل هو HELLO الله أحد») → needs_review/foreign_material, the run highlighted in the user's text. Allowed: punctuation, quotation marks, honorific ligatures, Quranic ornaments, ayah-number markers inside brackets («﴿١﴾», «(255)»). |
The tokenizer drops every non-letter, so the strict gate never saw the intrusion; both audit probes were found with empty notices. |
extract/foreign.py, pipeline._foreign_gate. |
| E-045 |
2026-10-03 |
Repeated quotes are merged only when byte-identical (B03). Key = raw quote text, not loose tokens; «قل هو الله أحد» + «قل هو الله احد» now get two verdicts (found / needs_review). |
The second occurrence inherited the first's verdict and its own hamza error vanished. |
pipeline.check, test_safety_gates.py. |
| E-046 |
2026-10-03 |
Unverifiable vocalisation is never a silent found: when a quote carries marks but no winning position can be aligned letter-for-letter → needs_review/diacritic_unverified (letters matched, marks not machine-verified). |
Audit B01: «اختلاف عدد حروف الرسم يعيد لا تعارض بدل حالة تعذر التحقق». |
pipeline._harakat_gate. |
| E-047 |
2026-10-03 |
English gate = data + deterministic retrieval only (docs/ENGLISH_GATE.md). Sources strictly from the challenge-approved list: QuranEnc API english_saheeh (prio 1) + english_rwwad (prio 2), HadeethEnc API EN for the 3 582 ids already in our Arabic corpus (2 328 have English; the 1 254 without are listed, not hidden). sha256-pinned in a new top-level manifest.json["translations"] section (the sources list is untouched); texts git-ignored. Retriever = unigram+bigram BM25 normalised by query idf mass, bigram weight 0.2 chosen by a measured sweep (only setting where every positive top-1 > every negative top-1 at max recall/MRR); total deterministic order; one candidate per (kind, ref). Candidate.translation_text is the upstream string byte-for-byte. No LLM, no judgment, no state. |
Red lines 1–2; ADR-002 §7 (no embeddings). Measured: recall@5 0.957 (22/23, indicative), negatives 7/7 < 0.60, 0.9 ms/query, pkl sha reproduced twice. The one miss (EH-015) is a synonym gap, documented. |
corpus/fetch_translations.py, backend/app/retrieve/translations.py, corpus/build_index.py (additive step), eval/english_cases.yaml, eval/run_english.py, backend/tests/test_translations.py |
| E-048 |
2026-10-03 |
English gate wired into the pipeline (english_gate.py): non-Arabic quotes keep I7 and carry english_candidates (approved translations cross-referenced to byte-exact Arabic records) with at most one selected by a deterministic rule (top-1 ≥ 0.90, gap ≥ 0.30) or, when the rule abstains, by a model constrained to an index / refusal (OpenAICompatPicker, parse_pick). Model ids recorded verbatim. |
Measured on 30 cases × 2 repeats: rule-only 11/23 correct · 0 wrong; gpt-5.4 21/23 · 0 wrong; claude-sonnet-5-5 22/23 · 0 wrong; claude-opus-5-5 22/23 · 0 wrong; negatives 0/7 false selections in every arm. This is the «with / without model» evidence for criterion 1 (25 %). |
eval/run_english_picker.py, docs/ENGLISH_GATE.md §7, tests/test_english_gate.py (17). |
| E-049 |
2026-10-03 |
Developer gate: one transport-agnostic core (app/devgate.py) behind three doors — MCP at /mcp (BASIRA_MCP=1, same process, same Pipeline, stateless streamable HTTP, SDK mcp 2.3 MCPServer; tools verify_text, verify_quote, guard_answer, list_sources, grounding_rules), REST additions (GET /v1/rules, POST /v1/guard, X-Basira-Determinism-Hash header on /v1/check and /v1/guard, SourceInfo.sha256, real OpenAPI examples for the four states captured from the full corpus), and the Guard (app/guard.py): clear iff every quotation is found, flagged otherwise, no_quotes; summaries from fixed templates in app/guard_messages.py (counts only, Arabic number agreement, footer «ليس حكمًا»), kept separate from messages/*.json. Tool names say verify, not check, to sit next to the association's 11 retrieval tools on mcp.islamiccontent.org (verified live: fetch with theirs → verify with ours, scripts/mcp_demo.py). |
INTEGRATIONS §2 gap (no verification primitive exists); red lines 1–3 enforced at the shared core (only source_text is religious text; scan_forbidden on every authored string incl. tool descriptions and rules; nothing stored). Measured: 38 new tests (217 total), REST ≡ MCP byte-equal hashes, snapshot boot 1.78 s / 315 MB with MCP mounted. |
backend/app/{devgate,guard,guard_messages,mcp_server}.py, main.py/schemas.py/config.py (additive), docs/API.md, docs/GUARD.md, docs/INTEGRATIONS.md §3.1, scripts/mcp_demo.py |