Method Build
Build(string, IReadOnlyList<RagSource>)
Finds [n] markers in a completed answer and builds one
CitationAnnotation { Title, Url, FileId, Snippet } per distinct marker, each carrying
a TextSpanAnnotatedRegion(StartIndex, EndIndex) per occurrence. A marker with no
matching source is left as plain text, counted, and never fabricated into a citation.
A total function: it does not throw.
public static IReadOnlyList<CitationAnnotation> Build(string answer, IReadOnlyList<RagSource> sources)
Parameters
answerstringThe complete answer text, and the string every returned
StartIndexandEndIndexindexes. On the streaming path that is the concatenation of every text delta in the response; after aggregation it isChatResponse.Text. They are the same string, which is why one set of offsets serves both.sourcesIReadOnlyList<RagSource>The ranked, clamped,
Ordinal-stamped list the block was rendered from. A marker is matched againstRagSource.Ordinal, never against list position, so an unstamped list resolves nothing rather than citing the wrong chunk.
Returns
Remarks
EndIndex is exclusive: answer[StartIndex..EndIndex] is exactly the marker text.
Both are always set, so a consumer's EndIndex - StartIndex needs one null check and no
fallback. A marker inside a code fence is treated like any other marker - the scan is
lexical, and that is documented behaviour rather than an oversight.