Skip to content

Search & verification

Verification engine

3 min readUpdated Verified against 7a343c6 on 2026-09-25

Page actions

ChatGPT and Claude are third-party services. The link gives them this page's public address and a short instruction. This site records nothing.

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.

/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.

The verification engine The claim is NFC-normalised and its script detected. Gurmukhi claims are cleaned of dandas and numerals; Roman claims are folded with roman_norm. Candidate lines come from the FTS index (an exact phrase, then a looser OR). Each candidate is scored with difflib’s ratio against the claim. The ladder then decides: an exact byte match is VERIFIED_EXACT; a claim contained whole in a line is VERIFIED_PARTIAL; a best ratio of at least 0.95 is VERIFIED; at least 0.85 is PROBABLE, or AMBIGUOUS when the runner-up is within 0.05 and itself at least 0.80; anything lower is NOT_FOUND with no line asserted. A claimed Ang adds +ANG_MATCH or +ANG_MISMATCH with the actual Ang. 07 · The verification engine Is this quotation really in the Granth? /api/verify answers with a verdict, aconfidence and the canonical line — never a guess The verdict ladder — the first rung that fits wins 1 · Normalise — detect the script1 · Normalisedetect the script2 · Candidates — from the fts index2 · Candidatesfrom the fts index2 · Score each — SequenceMatcher2 · Score eachSequenceMatcher3 · VERIFIED_EXACT · confidence 1.0 — the cleaned bytes equal a canonical line3 · VERIFIED_EXACT · confidence 1.0the cleaned bytes equal a canonical lineno4 · VERIFIED_PARTIAL · confidence 0.95 — the claim sits whole inside a canonical line4 · VERIFIED_PARTIAL · confidence 0.95the claim sits whole inside a canonical lineno5 · VERIFIED · confidence = the ratio — best ratio ≥ 0.955 · VERIFIED · confidence = the ratiobest ratio ≥ 0.95_THRESH_EXACT 0.95 · _THRESH_PROBABLE 0.85 — _THRESH_AMBIG 0.80 · _THRESH_GAP 0.05_THRESH_EXACT 0.95 · _THRESH_PROBABLE 0.85_THRESH_AMBIG 0.80 · _THRESH_GAP 0.05no6 · PROBABLE — best ≥ 0.85, runner-up far6 · PROBABLEbest ≥ 0.85, runner-up far6 · AMBIGUOUS — two lines fit: no choice6 · AMBIGUOUStwo lines fit: no choiceno7 · NOT_FOUND · confidence 0.0 — best ratio under 0.85, or no candidate at all7 · NOT_FOUND · confidence 0.0best ratio under 0.85, or no candidate at all7 · The Ang modifier — +ANG_MATCH or +ANG_MISMATCH7 · The Ang modifier+ANG_MATCH or +ANG_MISMATCHThe answer — verdict, confidence, the lineThe answerverdict, confidence, the linecontract/golden_verify.ndjson pins the verdicts — replayed against every deploy by contract_http.pycontract/golden_verify.ndjson pins the verdictsreplayed against every deploy by contract_http.py component in this projectdata at restproven, verified statenot found: no line is assertedexplanationflow: data or control moves this way Source of truth: webapp/verify.py · webapp/romannorm.py ·webapp/sggs/verification.py · contract/golden_verify.ndjson v1.3.10 · verified 2026-09-27 · d85c2adSri Guru Granth Sahib Ji — Knowledge Base · engineering wiki
Poster 07 — The verification engine: normalise, candidates, scoring, and the verdict ladder from VERIFIED_EXACT to NOT_FOUND with the Ang modifier · open full size

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 to use it, or call GET /api/verify?q=…&ang=… on the production API yourself (the 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.

RungVerdictConditionConfidence
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()—
3VERIFIED_EXACTthe phrase query produced an exact hit (the cleaned bytes equal a line)1.0
4VERIFIED_PARTIALthe claim (≥ 3 words or ≥ 12 characters) sits whole inside a candidate line, and is not the whole line0.95
5VERIFIEDbest ratio ≥ 0.95 (_THRESH_EXACT)the ratio
6PROBABLEbest ratio ≥ 0.85 (_THRESH_PROBABLE) and the runner-up is not closethe ratio
6AMBIGUOUSbest ≥ 0.85, runner-up ≥ 0.80 (_THRESH_AMBIG), gap < 0.05 (_THRESH_GAP): two lines fitthe ratio
7NOT_FOUNDbest ratio < 0.85, or no candidates0.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.

webapp/verify.py · _make_verdict · lines 207–284
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),
},
}

Read from the source at build time — view these lines on GitHub at c3d9360.

Source: webapp/verify.py · _make_verdict — the ladder, read from the file at build time.

The response

{
"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.