Skip to content

Latest commit

 

History

History
96 lines (88 loc) · 42.8 KB

DECISIONS.md

File metadata and controls

96 lines (88 loc) · 42.8 KB

DECISIONS.md — Decision log (product + engineering)

Append-only. D- = product/policy decisions, E- = engineering decisions. A decision is changed only by a new entry that supersedes it.

Product decisions

ID Date Decision Reason / intent Affects
D-001 2026-09-30 Single integration branch. Changes land on main only after every gate is green and re-verified after merge. One maintainer; the gates are the review. Git workflow
D-002 2026-09-30 Build in the open from the first commit. History is kept; nothing is squashed into a single import. Traceability for reviewers. Repository
D-003 2026-09-30 The repository explains itself: architecture, decisions, evaluation and deployment are documented next to the code. A newcomer must be able to build, test and deploy from the README alone. docs/
D-004 2026-09-30 Offline by default. Every component runs without network or provider accounts; model providers are optional and configured at deploy time. Reproducible tests and evaluation. config.py, providers/
D-005 2026-09-30 Data sources are limited to the challenge-approved, openly licensed corpora (Tanzil, Open-Hadith-Data, HadeethEnc), each sha256-pinned. Licensing and provenance. corpus/manifest.json, SOURCES.md
D-006 2026-09-30 Attribution: the project is the participant's work; AI tools used during development are disclosed in AI_USAGE.md, never presented as authors. Honest disclosure. AI_USAGE.md
D-007 2026-09-30 Guard API, OHD display and HadeethEnc display mode were decided explicitly (see Q1–Q4 below) before they shipped. No silent defaults on licensing or scope. Q1–Q4

Engineering decisions

ID Date Decision Reason ADR
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

Scope questions and the answers applied

Q Question (plain language in docs/GLOSSARY.md) Answer applied
Q1 Code licence — Apache-2.0 recommended Apache-2.0 (LICENSE)
Q2 HadeethEnc: show full hadith text (embed) or only grade+link (link)? link (safer)
Q3 Guard API (public API for other apps) stays a conditional extension, not a promise? Shipped as POST /v1/guard (E-049)
Q4 Open-Hadith-Data upstream copyright unresolved — keep with full documentation + kill-switch? Keep, OHD_MODE=display, documented
E-050 2026-10-03 UI v3 «البصيرة». Multi-page platform (/, /check, /services, /developers, /trust(+#limits), /about), Lens hero driven by a recorded verbatim /v1/check response (scripts/gen_examples.py --record), examples generated from eval/cases.yaml + scripts/smoke.py, trust numbers generated from report lines (scripts/gen_trust.py, vitest re-checks each number on its cited line), lazy WebGL khatam backdrop (capable devices only, off with reduced-motion), PWA shell SW (never caches /v1/*). Not-built services (English, Guard, MCP, receipt) shown only as «قريبًا», never simulated. Backend touched once: GZipMiddleware (bundle served by the API: 340 kB → 99 kB gz). Removed a hand-typed verse from the input placeholder.
E-051 2026-10-03 Server-side model configuration (/settings). The Genspark key is entered once, verified upstream with a 5-token call, and saved on the server (backend/.runtime/model.json, mode 0600, git-ignored; override path BASIRA_MODEL_CONFIG). PUT /v1/models/config {api_key?, model} verify-then-save and hot-swap the live LLM/vision/picker in-process (same pipeline object); DELETE restores env defaults; GET /v1models = curated catalog + config with the key masked (last 4). No per-request key headers exist (the browser never resends the key). Model id must be in the measured catalog (10 models, docs/MODELS.md; default gpt-5.4-mini 6/6 · 1.2 s · 0.3×). Verdicts are model-independent: same determinism_hash with mock or configured provider (test). Also fixed: app.mount("/", mcp_app) shadowed the SPA fallback (site 404 with BASIRA_MCP=1) → exact Route("/mcp") + regression test.
E-052 2026-10-03 **Verification receipt + V6 + attribution-only **. (A) Stateless receipt: POST /v1/receipt and GET /v/{token}?h= share one core (devgate.issue_receipt / verify_receipt); token = base64url(zlib(json{v:1,t,l})) — the receipt is the input, receipt_id = determinism_hash[:16]; verified_now iff the fresh hash equals ?h=, stale iff it differs (stated, never hidden); MCP tool #6 issue_receipt; error receipt_invalid (400). (B) V6 in verify.py: every found is re-proved from the Store — strict tokens of the quote must be a contiguous window of the matched record's strict stream, checked in both rasm streams (GS@g_start, GS2@g2_start) with twin alternates; fail → needs_review/validator_unproven (matches kept), validator_rejections += 1. (C) B05: isnad-only / attribution-only quotes → needs_review/attribution_only with a neutral message, matches=[], not counted as a rejection; a chain verbatim in an OHD record stays found.
E-053 2026-10-03 Correctness fixes B06/B07/B10/B12, each one measured before/after on the fixture pipeline. B06: _facts picks, among the strict-exact Quran winners, the position that lies inside the claimed ayah/range (else the first) — «﴿فبأي آلاء ربكما تكذبان﴾ [الرحمن: 77]» was claimed_source_mismatch=True before, False after; [البقرة: 5] stays a mismatch. New app/quran_meta.py (AYAH_COUNTS, Σ 6 236, asserted at import and test-pinned equal to corpus/data/tanzil-uthmani.txt surah-by-surah) → notice claimed_ref_invalid for impossible refs; status untouched (I8). B07: Match.source_segments[] — one verbatim record per covered ayah (records_covering), V1/V2 per element in verify._segments_ok; the UI lists them (QuoteCard, <ol class="segments">). B10: quotes_detected / extraction_truncated on CheckResponse + notice on every returned quote; image path: ocr_truncated field + notice. B12: client_identity(peer, xff, trusted) — XFF honoured only when the peer ∈ BASIRA_TRUSTED_PROXIES, right-most untrusted hop; Dockerfile drops --forwarded-allow-ips='*'; sniff_image_mime (PNG/JPEG/WebP signatures) before the provider. B13: already fixed on main by UI v3 (pickExample); verified lint 0 errors here.
E-054 2026-10-03 Frontend tells the truth about what is live. UI v3 shipped with English, Guard, MCP and the receipt marked «قريبًا» while all four were live on main (E-048/E-049/E-052). Wired, not redesigned: English mode = same /v1/check with english_candidates[] rendered on the QuoteCard (approved translations + verbatim Arabic, selected marked); Guard mode = POST /v1/guard → one verdict panel (clear/flagged/no_quotes, fixed summaries); receipt = button under the pipeline strip → POST /v1/receipt → shareable /v/{token}?h= link; MCP badge → live, six tools listed; developers table gains guard/receipt/replay/rules/models/mcp rows. Two tests that encoded the «soon» state were rewritten to assert the live behaviour.
E-055 2026-10-05 Mobile result reveal + small-screen fixes (E-UX-01…05) and design dossier (docs/design/). After «افحص» the page now scrolls to and focuses the report (effect after commit, not setTimeout); a new check clears the previous report; errors are focused; #results carries aria-busy; short phones (≤700 px tall) and 320 px widths keep the button, status strip and header on screen. Identity v4 «ختم المعارضة» (8-fold khatam + dāra + rhombic nuqta) documented as a proposal with mockups, SVG assets and a geometry guard (seal.py --check: 8 tips at 45°, never a hexagram).
E-056 2026-10-05 Identity v4 «ختم المعارضة» implemented. One layer frontend/src/site/identity.css loaded last; seal (8-fold khatam of two squares + dāra window + gold rhombic nuqta) generated from docs/design/brand/seal.py into Logo.tsx, favicons, PWA icons, OG cards and logo files; the four state-* icons become the dāra glyphs (filled rhombus / hollow rhombus / bar / empty ring); night band + khatam lattice + gold chain; jadwal frame; Naskh headings (Amiri Bold) and Kufic wordmark (Reem Kufi), both OFL, 112 316 B. Eye-mark files deleted.
E-057 2026-10-05 CI activated; non-product material removed from the tree. The workflow lives in .github/workflows/ci.yml and builds the sha256-pinned corpora → index → fixture before tests, so corpus-backed tests run instead of skipping.
E-058 2026-10-05 Release 0.3.1 hygiene. Runtime deps and Python 3.12 declared; run_1b.py aligned with Pipeline.check() and IslamicEval re-measured (1B 78.54 %, false confirmations 2); docs point only at public documents; BUILD_SHA passed into the image.
E-059 2026-10-06 Telegram bot as a thin API client (integrations/telegram). Long polling by default: no public port, no TLS endpoint, no webhook secret to manage, works behind NAT; webhook is opt-in. No storage: text and image bytes exist only for one request (ADR-004); logs carry counts and latency only. Every sentence about a quotation is a messages/*.json template; bold ranges come from the API diff.
E-060 2026-10-06 Bot replies as Telegram Rich Messages (Bot API 10.3). One RTL rich message per result: heading + status strip, the user's quote as a cited blockquote, source_text as a pull-quote credited with ref_label_ar (one per ayah), <mark> from the API diff, a compact metadata table, collapsible notices/positions/full record, in-message url/copy buttons. Classic HTML (4 096-char split) is the automatic fallback on any rejected rich call.
E-061 2026-10-06 Whole-ayah anchor. extract/anchor.detect accepts an unmarked run that spans a complete ayah (after the basmala offset, either rasm) regardless of word rarity.
E-062 2026-10-06 Phrase-rarity anchor + backward extension + isnad guard. extract/anchor.detect accepts an unmarked run of ≥ 3 tokens that occurs ≤ 60× verbatim in the whole corpus (with ≥ 1 content word, not isnad-shaped), and extends every run backwards over leading stop-words. Composer shows «لا يلزم قال تعالى… ولا الهمزات».
E-063 2026-10-06 Bot runs on the VPS; «open in Basira» carries the text. Production overlay reads the token from /etc/basira/telegram-bot.env (600 root) via env_file: !override; a random BASIRA_EVAL_KEY shared by /etc/basira/basira.env and the bot lets the API skip its per-IP limiter for the bot (all Telegram users otherwise share one 30 req/min budget behind one Docker address) while the bot's 10 msg/min per user stays the guard; COMPOSE_PROFILES from autodeploy.conf is exported by autodeploy.sh, which logs `bot: healthy
E-064 2026-10-06 Stable-named static files revalidate; only /assets/* is immutable. The SPA catch-all served favicon.ico, sw.js and manifest.webmanifest as public, max-age=31536000, immutable, so browsers that visited before identity v4 (E-056) kept the old eye favicon for up to a year. Now: /assets/* (content-hashed) immutable; sw.js/manifest no-cache; everything else max-age=3600, must-revalidate. Icon links in index.html carry ?v=4, the service worker cache is bumped to basira-shell-v3, and og:image/twitter:image are absolute URLs (crawlers reject relative ones) with og:url + canonical.
E-065 2026-10-06 Home page sections never stayed hidden again. useReveal ran on every render; the re-render caused by the WebGL backdrop mounting (~1 s after load on capable devices) tore down the IntersectionObserver and the safety timer, and the next run matched [data-reveal]:not([data-in]) = nothing, so every section below the hero stayed at opacity 0 — the page looked empty after the first screen. Now the effect runs once per mount, re-adopts already-hidden elements, and reveals everything on cleanup.