Skip to content

feat: grade hadith citations by sanad strength - #63

Merged
zeemscript merged 2 commits into
Deen-Bridge:devfrom
Tijesunimi004:feat/hadith-authenticity-grading
Jul 23, 2026
Merged

feat: grade hadith citations by sanad strength#63
zeemscript merged 2 commits into
Deen-Bridge:devfrom
Tijesunimi004:feat/hadith-authenticity-grading

Conversation

@Tijesunimi004

@Tijesunimi004 Tijesunimi004 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #53. Adds a hadith authenticity grading layer so the assistant never presents a weak or fabricated narration as authentic without saying so.

  • hadith.py parses hadith references out of a model answer, normalizes collection names/numbering across common phrasings ("Sahih al-Bukhari 1" / "Bukhari [Enhancement] AI service responds slowly with no streaming or caching #1" / etc.), and looks each one up in a bundled grading dataset covering Sahih al-Bukhari, Sahih Muslim, the four Sunan, and Muwatta Malik.
  • Each grader's raw grade string is decomposed into two independent axes: authenticity strength (sahih/hasan/da'if/mawdu') and chain type (marfu/mauquf/maqtu/mursal). A mauquf or maqtu report is not actually a saying of the Prophet regardless of chain strength, so it's never presented as one.
  • When a hadith has multiple graders who disagree, the weakest strength and most cautious chain type win (safety-first), but every individual grader's raw grade is preserved in the record.
  • The dataset is built by scripts/build_hadith_data.py from the public-domain fawazahmed0/hadith-api (pinned tag 1, Unlicense). Only grading metadata is bundled, never hadith text, which sidesteps translation-copyright questions entirely. Full provenance and the normalization policy are documented in data/hadith/PROVENANCE.md.
  • main.py appends a caution note to any answer that leans on a da'if/mawdu' hadith without already caveating it, or where an unverified reference is the sole hadith support in the answer. A weak hadith mentioned for targhib/tarhib with an explicit caveat is left alone. ChatResponse gains an optional hadith_references field (additive — existing clients unaffected) so a frontend can render authenticity badges. The system prompt gets explicit hadith-citation rules.
  • Bukhari and Muslim get no per-hadith grade from the source data because classical scholarship treats both as authenticated by consensus (ijma') in their entirety — that's encoded as grade SAHIH with grader "Scholarly consensus (Bukhari and Muslim)", not left as a gap.
  • CI now lints/compiles hadith.py and the build script, and runs the new offline test suite.

Test plan

  • flake8 main.py stellar.py safety tests/redteam study.py fiqh.py hadith.py scripts/build_hadith_data.py --max-line-length=120 --ignore=E501,W503
  • python -m compileall -q main.py stellar.py safety tests/redteam study.py fiqh.py hadith.py scripts/build_hadith_data.py
  • pytest -q tests/test_hadith.py (72 tests: grade-string tokenizer against real dataset strings, collection alias normalization, reference parsing, policy enforcement for weak/mawdu/mauquf/unverified-as-sole-evidence cases, and integration smoke tests against the real bundled dataset)
  • pytest -q tests/redteam tests/test_study.py tests/test_semantic_cache.py tests/test_fiqh.py — full existing suite still green
  • Manual end-to-end smoke test of /chat with a mocked Gemini client: a da'if citation gets flagged with a caution note appended and structured hadith_references; a Sahih al-Bukhari citation is verified and unflagged with no caution appended

Summary by CodeRabbit

  • New Features
    • Added hadith citation detection and grading in chat responses, including structured reference details and adab/caution notes for weak, fabricated, or non-Marfū narrations.
    • Added bundled hadith grading data for “Muwatta Malik” and documentation covering dataset provenance.
  • Bug Fixes
    • Cached responses now include hadith annotations consistently with newly generated responses.
  • Tests
    • Added a comprehensive hadith parsing/grading/annotation test suite.
  • Chores
    • Expanded CI linting/syntax checks and added a dedicated CI step to run the new hadith tests.

Adds a hadith authenticity grading layer so the assistant never presents a
weak or fabricated narration as authentic without saying so.

hadith.py parses hadith references out of a model answer, normalizes
collection names/numbering across common phrasings, and looks each one up
in a bundled grading dataset covering Sahih al-Bukhari, Sahih Muslim, the
four Sunan, and Muwatta Malik. Each grader's raw grade string is decomposed
into authenticity strength (sahih/hasan/da'if/mawdu') and chain type
(marfu/mauquf/maqtu/mursal), since a mauquf or maqtu report is not actually
a saying of the Prophet regardless of how strong its chain is. When a
hadith has multiple graders who disagree, the weakest strength and most
cautious chain type win, and every individual grader's raw grade is kept
in the record so nothing is hidden.

The dataset is built by scripts/build_hadith_data.py from the public-domain
fawazahmed0/hadith-api (pinned tag 1), bundling only grading metadata
(never hadith text, sidestepping translation-copyright questions). Full
provenance and the normalization policy are documented in
data/hadith/PROVENANCE.md.

main.py now appends a caution note to any answer that leans on a da'if or
mawdu' hadith without already caveating it, or where an unverified
reference is the sole hadith support in the answer. Weak hadith mentioned
for targhib/tarhib with an explicit caveat are left alone. ChatResponse
gains an optional hadith_references field so a frontend can render
authenticity badges; existing clients are unaffected. The system prompt
gets explicit hadith-citation rules (name the collection, state the grade,
prefer the two Sahihs for evidentiary claims).

CI now lints and compiles hadith.py and the build script, and runs the new
offline test suite (tests/test_hadith.py).
Copilot AI review requested due to automatic review settings July 23, 2026 13:29
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 07b5d0c3-2996-47e8-92ba-ffdf73c88bef

📥 Commits

Reviewing files that changed from the base of the PR and between 4bac053 and 92d554e.

📒 Files selected for processing (1)
  • hadith.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • hadith.py

Walkthrough

Adds hadith grading and citation annotation, bundled metadata and generation tooling, caution policies, chat-response integration, tests, and CI coverage.

Changes

Hadith grading flow

Layer / File(s) Summary
Reference parsing and grading core
hadith.py
Defines grading taxonomies, normalizes collection aliases, parses citations, loads bundled records, annotates references, and generates caution notes.
Dataset provenance and generation
data/hadith/PROVENANCE.md, data/hadith/malik.json, scripts/build_hadith_data.py
Documents the dataset schema and provenance, adds Muwatta Malik metadata, and provides upstream dataset generation with consensus handling.
Chat response annotation
main.py
Adds hadith metadata to responses, injects hadith guidance into prompts, and annotates cached and generated responses.
Hadith behavior validation and CI
tests/test_hadith.py, .github/workflows/ci.yml
Tests parsing, grading, annotation, caution policies, and bundled data, while expanding lint, syntax, and test checks in CI.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ChatHandler
  participant HadithAnnotator
  participant BundledDataset
  Client->>ChatHandler: Submit chat request
  ChatHandler->>HadithAnnotator: Annotate generated or cached response
  HadithAnnotator->>BundledDataset: Look up collection and hadith number
  BundledDataset-->>HadithAnnotator: Return grading metadata
  HadithAnnotator-->>ChatHandler: Return references and caution note
  ChatHandler-->>Client: Return response with hadith references
Loading

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning [#53] Core grading, prompt, data, and response changes are present, but the required mocked Gemini handler tests aren't shown. Add offline handler tests with a mocked Gemini client to verify annotation output and weak-hadith flagging, then run them in CI.
Docstring Coverage ⚠️ Warning Docstring coverage is 2.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding hadith citation grading by strength.
Out of Scope Changes check ✅ Passed All changed files support hadith grading, provenance, prompt guidance, or CI/test coverage; no unrelated changes stand out.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces an offline hadith authenticity grading layer that parses hadith citations from model answers, normalizes references, looks them up in a bundled grading dataset, and enforces a safety-first policy by appending a caution note when weak/unverified narrations are used without appropriate caveats.

Changes:

  • Added hadith.py to parse/normalize hadith references, look up bundled grades, and generate a policy-driven caution note + structured HadithReference metadata.
  • Added an offline dataset build script and provenance documentation for the bundled grading metadata.
  • Integrated grading into /chat responses (including cache behavior) and extended CI to lint/compile/test the new module.

Reviewed changes

Copilot reviewed 6 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
hadith.py New grading/normalization/policy module and structured response metadata.
main.py Appends hadith citation rules to the system context and annotates responses with grading + caution notes.
scripts/build_hadith_data.py Builds the bundled grading dataset JSON from the pinned upstream source.
data/hadith/PROVENANCE.md Documents provenance, licensing rationale, and normalization policy.
.github/workflows/ci.yml Extends lint/compile and adds hadith test execution in CI.
tests/test_hadith.py Offline unit + integration tests for tokenization, parsing, aggregation, and policy behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread main.py
chat_id=chat_id,
history=cached.history,
fiqh=fiqh_info,
hadith_references=annotate_hadith(cached.response),
Comment thread hadith.py
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@hadith.py`:
- Around line 388-402: Update _has_nearby_caveat to use the existing
_contains_word matcher for each keyword instead of plain substring containment
in the nearby window. Preserve the current window bounds and caveat keyword
list, ensuring unrelated words such as “weakness” do not suppress warnings while
valid single- and multi-word caveats still match.

In `@main.py`:
- Around line 290-297: Prevent synchronous hadith grading from blocking the
event loop in the async chat handler: update both the normal response path
around annotate_hadith/build_caution_note and the cache-hit path to offload the
grading work with asyncio.to_thread, or warm get_default_source at application
startup so its cached JSON index is loaded before requests arrive. Preserve the
existing caution text and cache behavior.
- Around line 290-297: Update the semantic-cache HIT handling around
cached.response and annotate_hadith so re-annotation uses only the response text
before the appended caution note, not the full cached text. Preserve returning
the cached caution note to the user while ensuring hadith_references match the
original pre-caution annotation and do not duplicate citations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 065cde6d-1103-457e-9669-b52eaddaa1b2

📥 Commits

Reviewing files that changed from the base of the PR and between 091fe25 and 4bac053.

📒 Files selected for processing (13)
  • .github/workflows/ci.yml
  • data/hadith/PROVENANCE.md
  • data/hadith/abudawud.json
  • data/hadith/bukhari.json
  • data/hadith/ibnmajah.json
  • data/hadith/malik.json
  • data/hadith/muslim.json
  • data/hadith/nasai.json
  • data/hadith/tirmidhi.json
  • hadith.py
  • main.py
  • scripts/build_hadith_data.py
  • tests/test_hadith.py

Comment thread hadith.py
Comment on lines +388 to +402
_CAVEAT_KEYWORDS = (
"weak", "da'if", "daif", "fabricat", "not authentic", "not to be relied",
"not reliable", "should not be relied", "for encouragement", "targhib",
"tarhib", "not be used as evidence", "grading unverified", "unverified",
"not a strong basis", "narrated for",
)

_CAVEAT_WINDOW = 150


def _has_nearby_caveat(text: str, span: Tuple[int, int]) -> bool:
start = max(0, span[0] - _CAVEAT_WINDOW)
end = min(len(text), span[1] + _CAVEAT_WINDOW)
window = text[start:end].lower()
return any(keyword in window for keyword in _CAVEAT_KEYWORDS)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Caveat detection uses unbounded substring matching — risks false negatives on the exact safety check this PR exists for.

_has_nearby_caveat does keyword in window (plain substring containment) rather than reusing the file's own word-boundary helper _contains_word. Several keywords are common substrings of unrelated words: "weak" matches inside "tweak", "weaker", "weakness"; this means any incidental nearby occurrence of these substrings — even in unrelated context — will suppress the flagged warning for a genuinely unqualified da'if/mawdu' citation. That directly undermines the PR's stated goal of flagging weak/fabricated narrations used without qualification.

🛡️ Proposed fix: reuse the existing word-boundary matcher
 def _has_nearby_caveat(text: str, span: Tuple[int, int]) -> bool:
     start = max(0, span[0] - _CAVEAT_WINDOW)
     end = min(len(text), span[1] + _CAVEAT_WINDOW)
     window = text[start:end].lower()
-    return any(keyword in window for keyword in _CAVEAT_KEYWORDS)
+    return any(_contains_word(window, keyword) for keyword in _CAVEAT_KEYWORDS)

_contains_word already handles multi-word phrases correctly (\b anchors around the whole escaped phrase), so this is a drop-in fix. Worth adding a regression test alongside it, e.g. asserting a nearby "weakness of iman" (unrelated to the citation) does not suppress flagging of a genuinely unqualified da'if citation.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_CAVEAT_KEYWORDS = (
"weak", "da'if", "daif", "fabricat", "not authentic", "not to be relied",
"not reliable", "should not be relied", "for encouragement", "targhib",
"tarhib", "not be used as evidence", "grading unverified", "unverified",
"not a strong basis", "narrated for",
)
_CAVEAT_WINDOW = 150
def _has_nearby_caveat(text: str, span: Tuple[int, int]) -> bool:
start = max(0, span[0] - _CAVEAT_WINDOW)
end = min(len(text), span[1] + _CAVEAT_WINDOW)
window = text[start:end].lower()
return any(keyword in window for keyword in _CAVEAT_KEYWORDS)
_CAVEAT_KEYWORDS = (
"weak", "da'if", "daif", "fabricat", "not authentic", "not to be relied",
"not reliable", "should not be relied", "for encouragement", "targhib",
"tarhib", "not be used as evidence", "grading unverified", "unverified",
"not a strong basis", "narrated for",
)
_CAVEAT_WINDOW = 150
def _has_nearby_caveat(text: str, span: Tuple[int, int]) -> bool:
start = max(0, span[0] - _CAVEAT_WINDOW)
end = min(len(text), span[1] + _CAVEAT_WINDOW)
window = text[start:end].lower()
return any(_contains_word(window, keyword) for keyword in _CAVEAT_KEYWORDS)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hadith.py` around lines 388 - 402, Update _has_nearby_caveat to use the
existing _contains_word matcher for each keyword instead of plain substring
containment in the nearby window. Preserve the current window bounds and caveat
keyword list, ensuring unrelated words such as “weakness” do not suppress
warnings while valid single- and multi-word caveats still match.

Comment thread main.py
Comment on lines +290 to +297
# --- Hadith authenticity grading ---
# Baked into response_text *before* the cache write so a cached hit
# replays the same caution the user originally saw.
hadith_refs = annotate_hadith(response_text)
caution = build_caution_note(response_text, hadith_refs)
if caution:
response_text = f"{response_text.rstrip()}\n\n{caution}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Synchronous hadith grading work runs inline inside the async request handler.

annotate_hadith/build_caution_note (and, on the cache-hit path, the same call at line 217) do regex parsing plus — on the first request touching each collection — a blocking open()/json.load() via BundledGradingSource._index. Since chat is async def, this synchronous work runs directly on the event loop thread, so a cold-start request can briefly stall other concurrent requests while the JSON dataset loads.

Consider warming get_default_source() at app startup (so the lru_cached index is already populated before traffic arrives), or wrapping the calls in await asyncio.to_thread(...) if warming isn't practical.

As per path instructions for **/*.py: "Flag blocking calls inside async endpoints (network calls should be awaited or offloaded)."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@main.py` around lines 290 - 297, Prevent synchronous hadith grading from
blocking the event loop in the async chat handler: update both the normal
response path around annotate_hadith/build_caution_note and the cache-hit path
to offload the grading work with asyncio.to_thread, or warm get_default_source
at application startup so its cached JSON index is loaded before requests
arrive. Preserve the existing caution text and cache behavior.

Source: Path instructions


🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Cache write bakes the caution note into the cached text, then the cache-hit path re-annotates that same text — producing duplicated/inconsistent hadith_references on cache hits.

response_text (with the caution note already appended) is what gets stored via semantic_cache.put(...) at line 303. On a later semantic-cache HIT (line 217), hadith_references=annotate_hadith(cached.response) re-scans the entire cached text — which now includes the caution note's quoted citation strings (e.g. - "Sunan Abu Dawud 3" — graded daif — ...). Since parse_references just regex-scans for collection+number patterns, it matches the citation a second time inside the note itself, so a cache-hit response can report duplicate entries for the same hadith — and the duplicate can even end up with a different flagged value than the original (the note text itself contains caveat words like "daif", so _has_nearby_caveat returns True for the copy sitting inside the note). This makes the new hadith_references field unreliable specifically for cached responses, which is a meaningful chunk of production traffic.

Note the non-cached path (line 318) is fine — it uses hadith_refs computed before the caution note was appended, so it doesn't duplicate.

🩹 Minimal fix: re-annotate only the pre-caution-note portion of the cached text
+# hadith.py — export the marker so main.py doesn't hardcode it
+HADITH_CAUTION_MARKER = "Note on hadith authenticity in this answer:"
                 return ChatResponse(
                     response=cached.response,
                     chat_id=chat_id,
                     history=cached.history,
                     fiqh=fiqh_info,
-                    hadith_references=annotate_hadith(cached.response),
+                    hadith_references=annotate_hadith(
+                        cached.response.split(HADITH_CAUTION_MARKER, 1)[0]
+                    ),
                 )

A more robust long-term fix is to persist the computed hadith_refs alongside the cached entry (in semantic_cache) instead of re-deriving them from text — worth a follow-up if semantic_cache's schema can be extended.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@main.py` around lines 290 - 297, Update the semantic-cache HIT handling
around cached.response and annotate_hadith so re-annotation uses only the
response text before the appended caution note, not the full cached text.
Preserve returning the cached caution note to the user while ensuring
hadith_references match the original pre-caution annotation and do not duplicate
citations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants