Skip to content

CiteNexus

Retrieval finds passages. CiteNexus proves the answer follows from one that governs — in 14 scripts — and refuses when it can't.

The answer was in the corpus. Nobody searched the language it was written in.

Section titled “The answer was in the corpus. Nobody searched the language it was written in.”

An employee at the Chennai office asks, in English, what it costs to buy out a month of unserved notice. The number exists, it is exact, and it is in a Tamil annexure. Searched only in English, the retriever never reaches it — and CiteNexus does the only honest thing with what it has. It refuses.

Beforesearched in enRefused

How much must a confirmed employee at the Chennai office pay to buy out one unserved month of the notice period?

“I can’t answer that from the available evidence.”

The correct behaviour on the evidence it had — and useless to the person asking. The answer was sitting in the corpus the whole time, one script away.

cited: none · the Tamil annexure was never retrieved
Aftersearched in en, ta, teAnswered

How much must a confirmed employee at the Chennai office pay to buy out one unserved month of the notice period?

“INR 45,000”

An exact token that survives translation — the hardest cross-lingual case, because nothing about the English question looks like the Tamil passage that answers it. Reached because the English question was also searched in Tamil and Telugu.

cited to06-ta-notice-buyout-annexure·answer_language: en

One argument changed. Nothing else — same corpus, same models, same 22 English questions:

rag.ask(question, search_languages=("en", "ta", "te")) # default: ("en",)

Tamil leads this page for a measurement reason, not a decorative one. Tamil was already a claimed script in the baseline, so these rows isolate exactly one variable: the search fan-out. Telugu’s numbers are larger and mean less — Telugu was absent from the tokenizer’s script table before this run, so part of its lift is being able to tokenize the script at all. Same sitting, two changes, and a single run cannot separate them. Tamil can.

Three more English questions, each answered from a Tamil annexure only after the fan-out:

Asked in English Before After Cited
Days of notice for the extra festival leave? refused குறைந்தபட்சம் 15 நாட்கள் முன்னதாக 05-ta-chennai-leave-annexure
Weeks of paid maternity leave at Chennai? refused 26 வாரங்கள் ஊதியத்துடன் கூடிய மகப்பேறு விடுப்பு வழங்கப்படும் 07-ta-maternity-creche-annexure
Additional festival-leave days per leave year? 4 நாட்கள்4 நாட்கள் 05-ta-chennai-leave-annexure

That last row already worked before the change, and it is on this page for that reason. Six of the 22 questions turn on a Tamil annexure: one was answered before, four after. At n=6 that is a direction, not a rate.

Python, and only Python: ask() and the search-language fan-out are the facade. Go and JavaScript ship the deterministic core — the same 14-script tokenizer and the same gate — with no facade to hang a fan-out on. Their entry point is further down this page.

Measured over a 12-document English/Tamil/Telugu corpus, one live run per configuration (Jina embeddings + reranker, Gemini at temperature 0):

searched in en searched in en, ta, te
answered / abstained 8 / 14 16 / 6
cited the right document 75% 100%
answered when the corpus could ground it 44% 89%
groundedness 100% 100%
abstained when nothing could ground it 100% 100%

That last row is the whole point. It answers twice as often without answering one thing it shouldn’t have.

14 scripts are claimed in the Python reference — Latin, Cyrillic, Greek, Han, Hiragana, Katakana, Hangul, Arabic, Hebrew, Devanagari, Bengali, Tamil, Telugu, Thai — each pinned by a golden conformance vector. A script that is not claimed is reported by name and refused, before a model call is spent — the question is tokenized, but the script is flagged as unclaimed rather than silently ranked. Eleven scripts — Kannada, Malayalam, Gujarati, Gurmukhi, Oriya, Sinhala, Khmer, Lao, Myanmar, Georgian and Armenian — are named in the table and deliberately not claimed: “I cannot read this script” is a better answer than a plausible ranking. Languages →

Which language you search in and which you answer in are two knobs

Section titled “Which language you search in and which you answer in are two knobs”

search_languages decides what gets retrieved. answer_language decides what comes back. They are independent on purpose, and the evidence never votes on either. Take Dutch — same Latin script, nothing exotic — asked against any corpus you already have:

res = rag.ask("Hoeveel dagen opzegtermijn geldt er?", answer_language="auto")
res.answer # prose in Dutch — "auto" detects nl from the QUESTION
res.sources[0].passage # the citation, verbatim, in ITS language — never translated
res.sources[0].passage_language # what that passage actually is

Pass answer_language="nl" to force it unconditionally; leave it unset and you get the configured default_answer_language, a fixed value rather than a quiet inference. The one thing that never moves is the citation: a translated quote is not the evidence, so it stays in its source language and sources[*].passage_language tells you which one that is.

This is the documented behaviour of the answer-language rule, shown in Dutch — not a measured result. There is no Dutch corpus in this repository, so there are no Dutch numbers on this page.

If it can’t cite it, it won’t say it.

Five ways a grounded answer is still wrong

Section titled “Five ways a grounded answer is still wrong”

Every one of these produces output where every visible signal says trust me: verbatim text, a real document, a page, 100% groundedness. That is worse than an obviously ungrounded answer, because there is nothing to notice. Each is closed by a deterministic check, and each was closed because it was measured failing first.

1. The words came from the passage — the claim didn’t follow

Section titled “1. The words came from the passage — the claim didn’t follow”

The faithfulness gate was set containment: every answer token appears in the passage. Set containment is closed under reordering and deletion. Invert the parties to a transaction and you have the same token set; drop the not from a prohibition and you have a strict subset. Both pass.

Measured: nine adversarial answers, each false with respect to its own cited passage, across five domains. Accepted as grounded 9 of 9 in Python, 9 of 9 in Go, 9 of 9 in JavaScript — one contract, faithfully reproduced three times.

Now is_supported_v2 verifies per atomic claim, with ordered gapped containment and a polarity guard, so an answer that reuses the passage’s vocabulary to assert the opposite is rejected. Unsupported claims are droppedunsupported_claims_removed counts them — and the answer keeps what survives rather than collapsing whole. False rejection on a legitimate-support control set: 0 of 30. The nine attacks, the predicate that replaced set containment, and the pinned gap budget: The faithfulness gate →

2. Two sources disagree, and rank order quietly picks one

Section titled “2. Two sources disagree, and rank order quietly picks one”

When the corpus contradicts itself, a ranked list still returns something. The top hit wins, and you never learn there was a second answer.

CiteNexus detects conflict deterministically and surfaces it — it never resolves it. Resolution is a policy question that belongs to you. In strict mode it abstains and cites both sides, verbatim, in Result.sources.

Measured: 0 false conflicts on 27 hard negatives written to look like conflicts and not be, 0 on 22 unrelated pairs, and 0 on a held-out set of 10 written after the thresholds were frozen. Recall is 0.889 — this detector is deliberately precise, not exhaustive.

It is deterministic set arithmetic, so it is native in Python, Go and JavaScript alike, pinned by 132 shared vectors. On a non-Latin corpus only the numeric-value rule can fire — negation and antonym run off English wordlists — and the page below says exactly which cases that leaves uncaught.

Conflicting sources →

3. The cited source has no authority over the question

Section titled “3. The cited source has no authority over the question”

A Florida statute answered a Texas question with a real quotation, a correct citation, and all_claims_verified: true.

Measured on a live California landlord–tenant corpus: out-of-jurisdiction citations 4 → 0; questions that should abstain and did, 33% → 100%. Groundedness and citation stayed at 100% throughout — which is exactly the point. 100% groundedness with four wrong-jurisdiction citations is the whole reason authority had to exist.

Authority tiers are curator assertions supplied at ingest (authority={...}) — the library never infers standing from prose — and strict mode enforces a floor that withholds grounded-but-unauthoritative evidence. Authority →

4. The governing document is in a script the system can’t read

Section titled “4. The governing document is in a script the system can’t read”

The Tamil case at the top of this page refuses, which is survivable. The sharper failure is the one that answers. An employee in the Hyderabad office asks, in English, how many days of unused leave they may carry forward. The English handbook says “An employee may carry forward a maximum of 10 days.” — verbatim, correctly cited, every claim through the faithfulness gate, 100% groundedness. It is also wrong for the person asking: the binding Telugu annexure (09-te-hyderabad-leave-annexure, clause 2.12) caps Hyderabad at 5 రోజులు and states that it overrides.

Not fabrication — the gate stops that — but a true quotation of a document that does not govern, which is indistinguishable from a correct answer at the point of use. A Unicode tokenizer makes the authoritative document citable; the search_languages fan-out makes it reachable. Both are required; neither is sufficient. (Telugu is the vivid illustration and not the measurement, for the reason given above.) Cross-lingual corpus →

5. A model failed, and the corpus was silently poisoned

Section titled “5. A model failed, and the corpus was silently poisoned”

A Go embedder that timed out on the Nth chunk had no way to say so, so it returned a zero vector — and ingest indexed it. Nothing downstream can detect that: retrieval just quietly stops finding a document that is provably present.

Failure is now expressible in the seam itself: the embedding contract returns an error, and the write path independently refuses a provider that returns the wrong number of vectors, or a vector that is empty, wrong-dimension for the run, non-finite (NaN/±Inf) or all zeros — before anything is stored. That guard is now in all three implementations, in the same rejection order, pinned by 48 committed vectors (conformance/cases/vector_validation.json).

The primitive underneath: cite, or abstain

Section titled “The primitive underneath: cite, or abstain”
employee-nda.pdfAnswered

Can the employee disclose confidential information?

“The employee shall not disclose confidential information.”

Verbatim from the source. Every word verifiable.

cited toemployee-nda.pdf·page 3
employee-nda.pdfAbstained

What is the capital of France?

“I can’t answer that from the available evidence.”

No passage supports it — so it refuses rather than guesses.

no grounding found · refused, not fabricated

Answers are generated at temperature 0 from retrieved passages only, quoted verbatim, then checked claim by claim against the passage they cite. Nothing survives that check without a document and a page behind it. Nothing survives it at all → the single pinned refusal string.

package main
import (
"fmt"
"github.com/muthuishere/citenexus/golang/answer"
)
func main() {
corpus := []answer.Doc{
{DocumentID: "employee-nda", Text: "The employee shall not disclose confidential information."},
}
res := answer.Ask(corpus, "Can the employee disclose confidential information?", answer.DefaultTopK)
fmt.Println(res.Answer) // the verbatim quote — or the pinned refusal
fmt.Println(res.Evidence.Decision) // "answered" | "refused"
}

A docs set that only lists wins is not one you can plan against.

Subject scope is an open gap. An answer can pass every guard above — grounded, verbatim, top-authority, right jurisdiction — and still be about the wrong kind of thing. A residential-tenancy statute answering a commercial-lease question clears all three checks, correctly, because nothing in the cited sentence says which tenancies it governs. CiteNexus guarantees no ungrounded claim. It does not guarantee that a grounded, high-authority citation is on-scope. The failure, the four signals that were measured and rejected, and the mitigations you can apply today: Wrong subject, right source →

Numbers on this page are single live runs, not benchmarks. The models are non-deterministic even at temperature 0; the safety metrics (groundedness, citation, abstention, out-of-jurisdiction count) reproduced exactly across repeated runs, while rate metrics moved by about one question. Quote the safety metrics; treat the rates as illustrations.

Bring your own model

One line: swap the transport. Keep the shipped client — it still builds the request, applies headers, expands ${ENV} and parses the response — and pass a callable that moves the bytes. An in-process ONNX model needs no server and no socket. Swap the transport →

One core, three languages

Every deterministic decision is byte-for-byte identical across Go, JavaScript and Python, pinned by a shared suite of golden conformance vectors generated from the Python reference and replayed by every port, which must reproduce them exactly. Install →

Any document

PDF, DOCX, PPTX, XLSX, HTML, Markdown, CSV, plain text, or a URL — plus figures via an injected vision model. Idempotent by content hash, S3-native. Ingest →

Prove the corpus

Reconcile index against manifest to show the answers derive from exactly the corpus that was agreed — and revoke a document with proof every byte is gone. Regulated audit →

Score it, don't trust it

evaluate() scores groundedness, citation and expected-support against a golden CSV. It cannot score abstention — a blank expected earns expected-support only if the row was answered (evaluate.py:76), so a must-refuse row scores better when the model answers it. Gate on refusals by driving ask() and asserting Decision.refused. Evaluate →

Navigate, don't cite

An entity graph and a distilled wiki sit over the evidence. You browse the map — but every hit resolves down to a real cited passage before an answer exists.

Start with the failure you recognise — all nine scenarios → — or go straight to the Quickstart.