File size: 2,171 Bytes
06671aa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import type { ResearchBundleV1, ResearchSourceV1, WebCitationV1 } from "./types";

/**
 * Web citation handling (§30, §31). Keeps web citations separate from document
 * citations. The action model cites source IDs inline as [S<number>]. The
 * server accepts only IDs from the validated bundle and builds final citation
 * objects from server-owned source metadata. Model-generated URLs are ignored.
 */

const MARKER_RE = /\[S(\d+)\]/g;

/** Extract the ordered, de-duplicated source IDs actually referenced in text. */
export function extractReferencedSourceIds(text: string): string[] {
    const out: string[] = [];
    if (!text) return out;
    const seen = new Set<string>();
    let m: RegExpExecArray | null;
    MARKER_RE.lastIndex = 0;
    while ((m = MARKER_RE.exec(text)) !== null) {
        const id = `S${m[1]}`;
        if (!seen.has(id)) {
            seen.add(id);
            out.push(id);
        }
    }
    return out;
}

/** Build web citation objects only for referenced, validated sources. */
export function buildWebCitations(
    text: string,
    bundle: ResearchBundleV1 | null,
): WebCitationV1[] {
    if (!bundle) return [];
    const referenced = new Set(extractReferencedSourceIds(text));
    const byId = new Map<string, ResearchSourceV1>();
    for (const s of bundle.sources) byId.set(s.sourceId, s);
    const citations: WebCitationV1[] = [];
    for (const id of referenced) {
        const src = byId.get(id);
        if (!src) continue; // unknown ID ignored
        citations.push({
            sourceId: src.sourceId,
            title: src.title,
            url: src.url,
            publisher: src.publisher,
            publishedAt: src.publishedAt,
            sourceType: src.sourceType,
        });
    }
    return citations;
}

/** Unknown/referenced-but-missing IDs (for observability), never thrown. */
export function findUnknownSourceIds(text: string, bundle: ResearchBundleV1 | null): string[] {
    const referenced = extractReferencedSourceIds(text);
    if (!bundle) return referenced;
    const known = new Set(bundle.sources.map((s) => s.sourceId));
    return referenced.filter((id) => !known.has(id));
}