# Verification engine Is this quotation really in the Granth? How /api/verify normalises a claim, finds candidates, scores them and climbs the verdict ladder — the code's thresholds, and a playground over the live API. - Page: - Updated: 2026-09-25 - Verified against: `7a343c6` on 2026-09-25 - From the Sri Guru Granth Sahib Ji — Knowledge Base wiki. Sri Guru Granth Sahib Ji is quoted verbatim, cited by Ang, and never altered. --- `/api/verify?q=&ang=` answers one question: *is this quotation really in Sri Guru Granth Sahib Ji, and where?* It returns a **verdict**, a **confidence**, the **canonical line** with its Ang, and the scoring details. It never returns a line it is not sure about: below the thresholds the verdict is `NOT_FOUND` and no line is asserted. The engine is `webapp/verify.py`, stdlib only, importable and served by the `verify` bounded context. ![Poster 07 — The verification engine: normalise, candidates, scoring, and the verdict ladder from VERIFIED_EXACT to NOT_FOUND with the Ang modifier](https://docs.gurbanisoul.com/posters/07-verification-engine.svg) ## Try it On the rendered wiki this is a playground: paste a quotation (Gurmukhi, or romanised), optionally the Ang you believe it is on, and the verdict, the canonical line and the scoring details come back from the live API while the rung lights up on the poster. On GitHub, open [this page on the wiki](https://docs.gurbanisoul.com/search/verification-engine/) to use it, or call `GET /api/verify?q=…&ang=…` on the production API yourself ([the API](https://docs.gurbanisoul.com/api/)). ## The ladder The thresholds are module constants in `webapp/verify.py`; `tools/docs_check.py` fails the build if the numbers on the poster stop matching them. | Rung | Verdict | Condition | Confidence | |---|---|---|---| | 1 | — | NFC-normalise; detect the script. Gurmukhi: strip dandas, danda numerals, pipes. Roman: fold each word with `roman_norm` | — | | 2 | — | candidates from `fts`: an exact phrase (text / skeleton for Gurmukhi, `translit_norm` for Roman), then a looser OR; each scored with `difflib.SequenceMatcher(...).ratio()` | — | | 3 | `VERIFIED_EXACT` | the phrase query produced an exact hit (the cleaned bytes equal a line) | 1.0 | | 4 | `VERIFIED_PARTIAL` | the claim (≥ 3 words or ≥ 12 characters) sits whole inside a candidate line, and is not the whole line | 0.95 | | 5 | `VERIFIED` | best ratio ≥ **0.95** (`_THRESH_EXACT`) | the ratio | | 6 | `PROBABLE` | best ratio ≥ **0.85** (`_THRESH_PROBABLE`) and the runner-up is not close | the ratio | | 6 | `AMBIGUOUS` | best ≥ 0.85, runner-up ≥ **0.80** (`_THRESH_AMBIG`), gap < **0.05** (`_THRESH_GAP`): two lines fit | the ratio | | 7 | `NOT_FOUND` | best ratio < 0.85, or no candidates | 0.0 — no line asserted | **The Ang modifier.** When the caller claims an Ang and a line was matched, the verdict gains `+ANG_MATCH` or `+ANG_MISMATCH(actual=N)`. A mismatch is still a found line — the quotation exists, just not where the caller said. ```python def _make_verdict(candidates, exact_hit, claim_clean, claim_tn, is_gurmukhi, cur): if not candidates: return { "verdict": "NOT_FOUND", "confidence": 0.0, "matched_line_id": None, "_best_row": None, "distance_details": {"note": "no FTS hits"}, } scored = [] for rid, _ in candidates: row = _fetch_line(cur, rid) if row is None: continue ratio = _score(row, claim_clean, claim_tn, is_gurmukhi) scored.append((ratio, rid, row)) if not scored: return { "verdict": "NOT_FOUND", "confidence": 0.0, "matched_line_id": None, "_best_row": None, "distance_details": {"note": "scoring yielded no rows"}, } scored.sort(key=lambda x: -x[0]) best_ratio, best_rid, best_row = scored[0] second_ratio = scored[1][0] if len(scored) > 1 else 0.0 gap = best_ratio - second_ratio # Containment tier: the claim is a clean FRAGMENT of a canonical line # (people quote half-tuks constantly). Requires >= 3 words or >= 12 chars. if exact_hit is None and (claim_clean or claim_tn): needle = claim_clean if is_gurmukhi else claim_tn if needle and (len(needle) >= 12 or len(needle.split()) >= 3): for ratio, rid, row in scored: hay = _clean_gurmukhi(row["gurmukhi"]) if is_gurmukhi \ else (row.get("translit_norm") or "") # a claim that IS the whole line is not a fragment — let it fall through # to the ratio tiers (VERIFIED at 1.0) instead of mislabelling it PARTIAL if needle != hay and f" {needle} " in f" {hay} ": return { "verdict": "VERIFIED_PARTIAL", "confidence": 0.95, "matched_line_id": rid, "_best_row": row, "distance_details": {"note": "claim is a fragment of this full line", "fragment_len": len(needle)}, } if exact_hit is not None: verdict = "VERIFIED_EXACT" confidence = 1.0 elif best_ratio >= _THRESH_EXACT: verdict = "VERIFIED" confidence = best_ratio elif best_ratio >= _THRESH_PROBABLE: if second_ratio >= _THRESH_AMBIG and gap < _THRESH_GAP: verdict = "AMBIGUOUS" else: verdict = "PROBABLE" confidence = best_ratio else: verdict = "NOT_FOUND" confidence = 0.0 # no line is being asserted — a % here only misleads best_rid_out = best_rid if verdict != "NOT_FOUND" else None best_row_out = best_row if verdict != "NOT_FOUND" else None return { "verdict": verdict, "confidence": round(confidence, 4), "matched_line_id": best_rid_out, "_best_row": best_row_out, "distance_details": { "best_ratio": round(best_ratio, 4), "second_ratio": round(second_ratio, 4), "gap": round(gap, 4), "candidates_scored": len(scored), }, } ``` *`webapp/verify.py`, lines 207–284, read at `c3d9360`: * Source: [`webapp/verify.py` · `_make_verdict`](https://github.com/Algorythmos-AI/sggs-platform/blob/integration/webapp/verify.py) — the ladder, read from the file at build time. ## The response ```json { "verdict": "VERIFIED+ANG_MISMATCH(actual=1)", "confidence": 1.0, "matched_line_id": 5, "ang": 1, "gurmukhi": "…the canonical line, verbatim…", "raag": null, "author": null, "comp_id": 2, "section": "…", "distance_details": { "best_ratio": 1.0, "second_ratio": 0.0, "gap": 1.0, "candidates_scored": 1 } } ``` `gurmukhi` is always the **canonical** line from the database, never the caller's text echoed back; `comp_id` lets a client open the whole composition. `distance_details` is diagnostic: for a `NOT_FOUND` it says why (`no FTS hits`, or the ratios that fell short). ## Input handling - The claim is limited (`MAX_CLAIM_CHARS` in `webapp/sggs/verification.py`); an empty claim is HTTP 400. - FTS tokens are sanitised and quoted with a central empty-`MATCH` guard: a crafted quote or asterisk cannot break the query or leak an exception (hardened in v2.11.0). - The engine opens the database read-only and immutable, with `query_only` as a second fence — the same guarantees as the API's own connection factory. ## Pinned by the contract `contract/golden_verify.ndjson` records real claims and their full verdicts. `make contract` fails if the engine's behaviour drifts; `tools/contract_http.py` replays the same records over HTTP against every deploy and the iOS app asserts its Swift port against the same file. See [Harnesses and golden vectors](https://docs.gurbanisoul.com/search/harnesses-and-golden-vectors/).