@michaelborck/honeyguide-core 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +4 -0
  2. package/dist/align/outcomes.d.ts +75 -0
  3. package/dist/align/outcomes.js +266 -0
  4. package/dist/align/outcomes.js.map +1 -0
  5. package/dist/align/reasons.d.ts +11 -0
  6. package/dist/align/reasons.js +84 -0
  7. package/dist/align/reasons.js.map +1 -0
  8. package/dist/annotate/ladder.d.ts +1 -1
  9. package/dist/annotate/ladder.js +2 -2
  10. package/dist/annotate/ladder.js.map +1 -1
  11. package/dist/annotate/ollama.d.ts +3 -1
  12. package/dist/annotate/ollama.js +17 -11
  13. package/dist/annotate/ollama.js.map +1 -1
  14. package/dist/batch.d.ts +0 -0
  15. package/dist/batch.js +0 -0
  16. package/dist/batch.js.map +0 -0
  17. package/dist/brief.d.ts +30 -0
  18. package/dist/brief.js +70 -0
  19. package/dist/brief.js.map +1 -0
  20. package/dist/collection.d.ts +27 -0
  21. package/dist/collection.js +98 -0
  22. package/dist/collection.js.map +1 -0
  23. package/dist/config.d.ts +31 -1
  24. package/dist/config.js +10 -0
  25. package/dist/config.js.map +1 -1
  26. package/dist/enrich/page.d.ts +0 -0
  27. package/dist/enrich/page.js +0 -0
  28. package/dist/enrich/page.js.map +0 -0
  29. package/dist/enrich/sourceType.d.ts +0 -0
  30. package/dist/enrich/sourceType.js +0 -0
  31. package/dist/enrich/sourceType.js.map +0 -0
  32. package/dist/enrich/unpaywall.d.ts +28 -0
  33. package/dist/enrich/unpaywall.js +36 -0
  34. package/dist/enrich/unpaywall.js.map +1 -0
  35. package/dist/export/citations.d.ts +0 -0
  36. package/dist/export/citations.js +0 -0
  37. package/dist/export/citations.js.map +0 -0
  38. package/dist/export/lmsHtml.d.ts +0 -0
  39. package/dist/export/lmsHtml.js +16 -2
  40. package/dist/export/lmsHtml.js.map +1 -1
  41. package/dist/export/markdown.d.ts +0 -0
  42. package/dist/export/markdown.js +11 -3
  43. package/dist/export/markdown.js.map +1 -1
  44. package/dist/index.d.ts +18 -1
  45. package/dist/index.js +17 -1
  46. package/dist/index.js.map +1 -1
  47. package/dist/pipeline.d.ts +2 -2
  48. package/dist/pipeline.js +117 -12
  49. package/dist/pipeline.js.map +1 -1
  50. package/dist/rank/decision.d.ts +59 -0
  51. package/dist/rank/decision.js +124 -0
  52. package/dist/rank/decision.js.map +1 -0
  53. package/dist/rank/diversity.d.ts +0 -0
  54. package/dist/rank/diversity.js +0 -0
  55. package/dist/rank/diversity.js.map +0 -0
  56. package/dist/rank/embeddings.d.ts +41 -0
  57. package/dist/rank/embeddings.js +98 -0
  58. package/dist/rank/embeddings.js.map +1 -0
  59. package/dist/rank/reranker.d.ts +21 -0
  60. package/dist/rank/reranker.js +30 -0
  61. package/dist/rank/reranker.js.map +1 -0
  62. package/dist/rank/semantic.d.ts +14 -0
  63. package/dist/rank/semantic.js +58 -0
  64. package/dist/rank/semantic.js.map +1 -0
  65. package/dist/recheck.d.ts +0 -0
  66. package/dist/recheck.js +0 -0
  67. package/dist/recheck.js.map +0 -0
  68. package/dist/retrieval/arxiv.d.ts +24 -0
  69. package/dist/retrieval/arxiv.js +94 -0
  70. package/dist/retrieval/arxiv.js.map +1 -0
  71. package/dist/retrieval/books.d.ts +0 -0
  72. package/dist/retrieval/books.js +0 -0
  73. package/dist/retrieval/books.js.map +0 -0
  74. package/dist/retrieval/breaker.d.ts +16 -0
  75. package/dist/retrieval/breaker.js +42 -0
  76. package/dist/retrieval/breaker.js.map +1 -0
  77. package/dist/retrieval/cache.d.ts +8 -0
  78. package/dist/retrieval/cache.js +33 -0
  79. package/dist/retrieval/cache.js.map +1 -0
  80. package/dist/retrieval/candidates.d.ts +1 -1
  81. package/dist/retrieval/candidates.js +31 -7
  82. package/dist/retrieval/candidates.js.map +1 -1
  83. package/dist/retrieval/counterpoint.d.ts +0 -0
  84. package/dist/retrieval/counterpoint.js +1 -0
  85. package/dist/retrieval/counterpoint.js.map +1 -1
  86. package/dist/retrieval/europePmc.d.ts +32 -0
  87. package/dist/retrieval/europePmc.js +62 -0
  88. package/dist/retrieval/europePmc.js.map +1 -0
  89. package/dist/retrieval/openalexTopical.d.ts +0 -0
  90. package/dist/retrieval/openalexTopical.js +0 -0
  91. package/dist/retrieval/openalexTopical.js.map +0 -0
  92. package/dist/retrieval/paperMerge.d.ts +16 -0
  93. package/dist/retrieval/paperMerge.js +77 -0
  94. package/dist/retrieval/paperMerge.js.map +1 -0
  95. package/dist/retrieval/papers.d.ts +10 -5
  96. package/dist/retrieval/papers.js +51 -32
  97. package/dist/retrieval/papers.js.map +1 -1
  98. package/dist/retrieval/podcasts.d.ts +0 -0
  99. package/dist/retrieval/podcasts.js +0 -0
  100. package/dist/retrieval/podcasts.js.map +0 -0
  101. package/dist/retrieval/query.d.ts +0 -0
  102. package/dist/retrieval/query.js +1 -0
  103. package/dist/retrieval/query.js.map +1 -1
  104. package/dist/retrieval/rateLimit.d.ts +6 -0
  105. package/dist/retrieval/rateLimit.js +15 -0
  106. package/dist/retrieval/rateLimit.js.map +1 -0
  107. package/dist/retrieval/search/brave.d.ts +13 -0
  108. package/dist/retrieval/search/brave.js +32 -0
  109. package/dist/retrieval/search/brave.js.map +1 -0
  110. package/dist/retrieval/search/index.d.ts +13 -0
  111. package/dist/retrieval/search/index.js +22 -0
  112. package/dist/retrieval/search/index.js.map +1 -0
  113. package/dist/retrieval/search/searxng.d.ts +14 -0
  114. package/dist/retrieval/search/searxng.js +24 -0
  115. package/dist/retrieval/search/searxng.js.map +1 -0
  116. package/dist/retrieval/search/serper.d.ts +14 -0
  117. package/dist/retrieval/search/serper.js +38 -0
  118. package/dist/retrieval/search/serper.js.map +1 -0
  119. package/dist/retrieval/search/types.d.ts +17 -0
  120. package/dist/retrieval/search/types.js +2 -0
  121. package/dist/retrieval/search/types.js.map +1 -0
  122. package/dist/retrieval/videos.d.ts +0 -0
  123. package/dist/retrieval/videos.js +0 -0
  124. package/dist/retrieval/videos.js.map +0 -0
  125. package/dist/retrieval/web.d.ts +5 -6
  126. package/dist/retrieval/web.js +14 -27
  127. package/dist/retrieval/web.js.map +1 -1
  128. package/dist/types.d.ts +62 -0
  129. package/dist/types.js +0 -0
  130. package/dist/types.js.map +0 -0
  131. package/dist/verify/verify.d.ts +11 -2
  132. package/dist/verify/verify.js +13 -4
  133. package/dist/verify/verify.js.map +1 -1
  134. package/package.json +1 -1
@@ -0,0 +1,30 @@
1
+ import { createEmbeddingProvider } from './embeddings.js';
2
+ import { semanticScores } from './semantic.js';
3
+ import { OllamaDecisionReranker } from './decision.js';
4
+ /** Embedding-based re-ranking (ADR-0015). */
5
+ export class EmbeddingReranker {
6
+ provider;
7
+ name = 'embeddings';
8
+ constructor(provider) {
9
+ this.provider = provider;
10
+ }
11
+ score(topic, candidates) {
12
+ return semanticScores(topic, candidates, this.provider);
13
+ }
14
+ }
15
+ /**
16
+ * Pick the best available local re-ranker (ADR-0015, ADR-0016). A System One
17
+ * decision model is preferred when configured because it judges relevance
18
+ * directly; embeddings are the fallback. With neither, ranking is unchanged.
19
+ */
20
+ export function createReranker(config) {
21
+ if (config.decisionUrl) {
22
+ return new OllamaDecisionReranker(config.decisionUrl, config.decisionModel ?? 'tev1:0.8b', config.decisionApiKey);
23
+ }
24
+ if (config.embeddingUrl) {
25
+ const provider = createEmbeddingProvider(config);
26
+ return provider ? new EmbeddingReranker(provider) : null;
27
+ }
28
+ return null;
29
+ }
30
+ //# sourceMappingURL=reranker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reranker.js","sourceRoot":"","sources":["../../src/rank/reranker.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,uBAAuB,EAA0B,MAAM,iBAAiB,CAAC;AAClF,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAQvD,6CAA6C;AAC7C,MAAM,OAAO,iBAAiB;IAGC;IAFpB,IAAI,GAAG,YAAY,CAAC;IAE7B,YAA6B,QAA2B;QAA3B,aAAQ,GAAR,QAAQ,CAAmB;IAAG,CAAC;IAE5D,KAAK,CAAC,KAAa,EAAE,UAAuB;QAC1C,OAAO,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC1D,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,MAAsB;IACnD,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;QACvB,OAAO,IAAI,sBAAsB,CAC/B,MAAM,CAAC,WAAW,EAClB,MAAM,CAAC,aAAa,IAAI,WAAW,EACnC,MAAM,CAAC,cAAc,CACtB,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;QACxB,MAAM,QAAQ,GAAG,uBAAuB,CAAC,MAAM,CAAC,CAAC;QACjD,OAAO,QAAQ,CAAC,CAAC,CAAC,IAAI,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC3D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { Candidate } from '../retrieval/candidates.js';
2
+ import type { EmbeddingProvider } from './embeddings.js';
3
+ /** Cosine similarity of two vectors; 0 when either is empty or mismatched. */
4
+ export declare function cosineSimilarity(a: number[], b: number[]): number;
5
+ /** The text embedded for a candidate: enough to judge topical relevance. */
6
+ export declare function candidateText(candidate: Candidate): string;
7
+ /**
8
+ * Score each candidate against the topic by embedding similarity. Returns a
9
+ * map keyed by candidate object (identity), not by URL, because duplicate
10
+ * URLs are already deduplicated upstream.
11
+ */
12
+ export declare function semanticScores(topic: string, candidates: Candidate[], provider: EmbeddingProvider): Promise<Map<Candidate, number>>;
13
+ /** Highest score first; candidates without a score keep their relative order. */
14
+ export declare function orderByScores(candidates: Candidate[], scores: Map<Candidate, number>): Candidate[];
@@ -0,0 +1,58 @@
1
+ const MAX_TEXT_CHARS = 1200;
2
+ /** Cosine similarity of two vectors; 0 when either is empty or mismatched. */
3
+ export function cosineSimilarity(a, b) {
4
+ if (!a.length || a.length !== b.length)
5
+ return 0;
6
+ let dot = 0;
7
+ let normA = 0;
8
+ let normB = 0;
9
+ for (let i = 0; i < a.length; i++) {
10
+ dot += a[i] * b[i];
11
+ normA += a[i] * a[i];
12
+ normB += b[i] * b[i];
13
+ }
14
+ if (!normA || !normB)
15
+ return 0;
16
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
17
+ }
18
+ /** The text embedded for a candidate: enough to judge topical relevance. */
19
+ export function candidateText(candidate) {
20
+ return [candidate.title, candidate.venue, candidate.description]
21
+ .filter(Boolean)
22
+ .join('. ')
23
+ .slice(0, MAX_TEXT_CHARS);
24
+ }
25
+ /**
26
+ * Score each candidate against the topic by embedding similarity. Returns a
27
+ * map keyed by candidate object (identity), not by URL, because duplicate
28
+ * URLs are already deduplicated upstream.
29
+ */
30
+ export async function semanticScores(topic, candidates, provider) {
31
+ const scores = new Map();
32
+ if (!candidates.length)
33
+ return scores;
34
+ const texts = [topic, ...candidates.map(candidateText)];
35
+ const vectors = await provider.embed(texts);
36
+ if (vectors.length !== texts.length) {
37
+ throw new Error('embedding count did not match the request');
38
+ }
39
+ const [topicVector, ...candidateVectors] = vectors;
40
+ candidates.forEach((candidate, i) => {
41
+ scores.set(candidate, cosineSimilarity(topicVector, candidateVectors[i]));
42
+ });
43
+ return scores;
44
+ }
45
+ /** Highest score first; candidates without a score keep their relative order. */
46
+ export function orderByScores(candidates, scores) {
47
+ return candidates
48
+ .map((candidate, index) => ({ candidate, index, score: scores.get(candidate) }))
49
+ .sort((a, b) => {
50
+ const sa = a.score ?? Number.NEGATIVE_INFINITY;
51
+ const sb = b.score ?? Number.NEGATIVE_INFINITY;
52
+ if (sa !== sb)
53
+ return sb - sa;
54
+ return a.index - b.index;
55
+ })
56
+ .map((entry) => entry.candidate);
57
+ }
58
+ //# sourceMappingURL=semantic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantic.js","sourceRoot":"","sources":["../../src/rank/semantic.ts"],"names":[],"mappings":"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"}
package/dist/recheck.d.ts CHANGED
File without changes
package/dist/recheck.js CHANGED
File without changes
File without changes
@@ -0,0 +1,24 @@
1
+ import type { Candidate } from './candidates.js';
2
+ export interface ArxivEntry {
3
+ title: string;
4
+ authors: string[];
5
+ year: number | null;
6
+ /** Canonical abs URL, without a version suffix. */
7
+ url: string;
8
+ summary?: string;
9
+ }
10
+ /**
11
+ * arXiv preprint discovery (ADR-0013). Preprints are a structural blind spot
12
+ * for Crossref/OpenAlex discovery, and the Atom API is keyless and
13
+ * authoritative for its own corpus.
14
+ */
15
+ export declare function searchArxiv(topic: string): Promise<Candidate[]>;
16
+ /**
17
+ * arXiv treats a bare multi-word phrase as an impossible exact match, so
18
+ * build a phrase query from the topic's content phrases and OR them, the same
19
+ * way OpenAlex and Open Library are queried.
20
+ */
21
+ export declare function buildArxivQuery(topic: string): string;
22
+ /** Parse the flat, machine-generated Atom feed without an XML dependency. */
23
+ export declare function parseArxivFeed(xml: string): ArxivEntry[];
24
+ export declare function arxivEntriesToCandidates(entries: ArxivEntry[]): Candidate[];
@@ -0,0 +1,94 @@
1
+ import { extractPhrases } from './query.js';
2
+ import { throttle } from './rateLimit.js';
3
+ const ENDPOINT = 'https://export.arxiv.org/api/query';
4
+ const API_TIMEOUT_MS = 10_000;
5
+ const SERVICE = 'arxiv';
6
+ // arXiv's terms of use ask for at most one request every three seconds.
7
+ const MIN_INTERVAL_MS = 3100;
8
+ /**
9
+ * arXiv preprint discovery (ADR-0013). Preprints are a structural blind spot
10
+ * for Crossref/OpenAlex discovery, and the Atom API is keyless and
11
+ * authoritative for its own corpus.
12
+ */
13
+ export async function searchArxiv(topic) {
14
+ await throttle(SERVICE, MIN_INTERVAL_MS);
15
+ const searchQuery = buildArxivQuery(topic);
16
+ if (!searchQuery)
17
+ return [];
18
+ const params = new URLSearchParams({
19
+ search_query: searchQuery,
20
+ start: '0',
21
+ max_results: '8',
22
+ });
23
+ const res = await fetch(`${ENDPOINT}?${params}`, {
24
+ headers: { accept: 'application/atom+xml', 'user-agent': 'Honeyguide/1.0' },
25
+ signal: AbortSignal.timeout(API_TIMEOUT_MS),
26
+ });
27
+ if (!res.ok)
28
+ throw new Error(`HTTP ${res.status} from arxiv.org`);
29
+ return arxivEntriesToCandidates(parseArxivFeed(await res.text()));
30
+ }
31
+ /**
32
+ * arXiv treats a bare multi-word phrase as an impossible exact match, so
33
+ * build a phrase query from the topic's content phrases and OR them, the same
34
+ * way OpenAlex and Open Library are queried.
35
+ */
36
+ export function buildArxivQuery(topic) {
37
+ const phrases = extractPhrases(topic, 3);
38
+ if (phrases.length) {
39
+ return phrases.map((p) => `all:"${p}"`).join(' OR ');
40
+ }
41
+ const fallback = topic.replace(/["\\]/g, ' ').replace(/\s+/g, ' ').trim();
42
+ return fallback ? `all:${fallback}` : '';
43
+ }
44
+ /** Parse the flat, machine-generated Atom feed without an XML dependency. */
45
+ export function parseArxivFeed(xml) {
46
+ const entries = [];
47
+ for (const match of xml.matchAll(/<entry>([\s\S]*?)<\/entry>/g)) {
48
+ const block = match[1];
49
+ const title = block.match(/<title>([\s\S]*?)<\/title>/)?.[1];
50
+ if (!title)
51
+ continue;
52
+ const id = block.match(/<id>https?:\/\/arxiv\.org\/abs\/([^<]+)<\/id>/)?.[1];
53
+ if (!id)
54
+ continue;
55
+ const published = block.match(/<published>(\d{4})-/)?.[1];
56
+ const summary = block.match(/<summary>([\s\S]*?)<\/summary>/)?.[1];
57
+ entries.push({
58
+ title: clean(decodeXmlEntities(title)),
59
+ authors: [...block.matchAll(/<name>([\s\S]*?)<\/name>/g)]
60
+ .map((a) => clean(decodeXmlEntities(a[1])))
61
+ .filter(Boolean),
62
+ year: published ? Number(published) : null,
63
+ url: `https://arxiv.org/abs/${id.replace(/v\d+$/, '')}`,
64
+ summary: summary ? clean(decodeXmlEntities(summary)) : undefined,
65
+ });
66
+ }
67
+ return entries;
68
+ }
69
+ export function arxivEntriesToCandidates(entries) {
70
+ return entries.map((entry) => ({
71
+ title: entry.title,
72
+ url: entry.url,
73
+ format: 'paper',
74
+ authors: entry.authors,
75
+ year: entry.year,
76
+ description: entry.summary ? entry.summary.slice(0, 600) : undefined,
77
+ // arXiv preprints are freely readable by definition.
78
+ licensing: 'link-only',
79
+ origin: 'arxiv',
80
+ databaseAttested: true,
81
+ }));
82
+ }
83
+ function decodeXmlEntities(s) {
84
+ return s
85
+ .replace(/&lt;/g, '<')
86
+ .replace(/&gt;/g, '>')
87
+ .replace(/&quot;/g, '"')
88
+ .replace(/&#39;/g, "'")
89
+ .replace(/&amp;/g, '&');
90
+ }
91
+ function clean(s) {
92
+ return s.replace(/\s+/g, ' ').trim();
93
+ }
94
+ //# sourceMappingURL=arxiv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"arxiv.js","sourceRoot":"","sources":["../../src/retrieval/arxiv.ts"],"names":[],"mappings":"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"}
File without changes
File without changes
File without changes
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Per-host circuit breaker. After a few consecutive failures a host is
3
+ * skipped for a cooldown, so one dead API does not add latency to every
4
+ * request. The first success closes the circuit again.
5
+ */
6
+ export declare class CircuitBreaker {
7
+ private readonly threshold;
8
+ private readonly cooldownMs;
9
+ private readonly failures;
10
+ constructor(threshold?: number, cooldownMs?: number);
11
+ canRequest(host: string, now?: number): boolean;
12
+ recordFailure(host: string, now?: number): void;
13
+ recordSuccess(host: string): void;
14
+ reset(): void;
15
+ }
16
+ export declare const defaultBreaker: CircuitBreaker;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Per-host circuit breaker. After a few consecutive failures a host is
3
+ * skipped for a cooldown, so one dead API does not add latency to every
4
+ * request. The first success closes the circuit again.
5
+ */
6
+ export class CircuitBreaker {
7
+ threshold;
8
+ cooldownMs;
9
+ failures = new Map();
10
+ constructor(threshold = 3, cooldownMs = 60_000) {
11
+ this.threshold = threshold;
12
+ this.cooldownMs = cooldownMs;
13
+ }
14
+ canRequest(host, now = Date.now()) {
15
+ const state = this.failures.get(host);
16
+ if (!state)
17
+ return true;
18
+ if (state.until > now)
19
+ return false;
20
+ // Only an expired *open* circuit resets the count; a closed circuit keeps
21
+ // accumulating consecutive failures.
22
+ if (state.until > 0)
23
+ this.failures.delete(host);
24
+ return true;
25
+ }
26
+ recordFailure(host, now = Date.now()) {
27
+ const state = this.failures.get(host);
28
+ const count = (state?.count ?? 0) + 1;
29
+ this.failures.set(host, {
30
+ count,
31
+ until: count >= this.threshold ? now + this.cooldownMs : 0,
32
+ });
33
+ }
34
+ recordSuccess(host) {
35
+ this.failures.delete(host);
36
+ }
37
+ reset() {
38
+ this.failures.clear();
39
+ }
40
+ }
41
+ export const defaultBreaker = new CircuitBreaker();
42
+ //# sourceMappingURL=breaker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"breaker.js","sourceRoot":"","sources":["../../src/retrieval/breaker.ts"],"names":[],"mappings":"AAMA;;;;GAIG;AACH,MAAM,OAAO,cAAc;IAIN;IACA;IAJF,QAAQ,GAAG,IAAI,GAAG,EAAwB,CAAC;IAE5D,YACmB,YAAY,CAAC,EACb,aAAa,MAAM;QADnB,cAAS,GAAT,SAAS,CAAI;QACb,eAAU,GAAV,UAAU,CAAS;IACnC,CAAC;IAEJ,UAAU,CAAC,IAAY,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;QACvC,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QACxB,IAAI,KAAK,CAAC,KAAK,GAAG,GAAG;YAAE,OAAO,KAAK,CAAC;QACpC,0EAA0E;QAC1E,qCAAqC;QACrC,IAAI,KAAK,CAAC,KAAK,GAAG,CAAC;YAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAChD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,aAAa,CAAC,IAAY,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;QAC1C,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACtC,MAAM,KAAK,GAAG,CAAC,KAAK,EAAE,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QACtC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE;YACtB,KAAK;YACL,KAAK,EAAE,KAAK,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;SAC3D,CAAC,CAAC;IACL,CAAC;IAED,aAAa,CAAC,IAAY;QACxB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IAED,KAAK;QACH,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;CACF;AAED,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,cAAc,EAAE,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Small process-local JSON cache for retrieval APIs. Repeated topics and
3
+ * batch runs then cost nothing, and the upstream APIs see fewer requests.
4
+ * Keyed by full URL, so headers and query strings are part of the identity.
5
+ */
6
+ export declare function getCachedJson<T>(key: string, now?: number, ttlMs?: number): T | undefined;
7
+ export declare function setCachedJson(key: string, value: unknown, now?: number): void;
8
+ export declare function clearJsonCache(): void;
@@ -0,0 +1,33 @@
1
+ const cache = new Map();
2
+ const DEFAULT_TTL_MS = 10 * 60 * 1000;
3
+ const MAX_ENTRIES = 500;
4
+ /**
5
+ * Small process-local JSON cache for retrieval APIs. Repeated topics and
6
+ * batch runs then cost nothing, and the upstream APIs see fewer requests.
7
+ * Keyed by full URL, so headers and query strings are part of the identity.
8
+ */
9
+ export function getCachedJson(key, now = Date.now(), ttlMs = DEFAULT_TTL_MS) {
10
+ const entry = cache.get(key);
11
+ if (!entry)
12
+ return undefined;
13
+ if (now - entry.at > ttlMs) {
14
+ cache.delete(key);
15
+ return undefined;
16
+ }
17
+ // Refresh insertion order so the eviction below is least-recently-used.
18
+ cache.delete(key);
19
+ cache.set(key, entry);
20
+ return entry.value;
21
+ }
22
+ export function setCachedJson(key, value, now = Date.now()) {
23
+ if (cache.size >= MAX_ENTRIES) {
24
+ const oldest = cache.keys().next().value;
25
+ if (oldest !== undefined)
26
+ cache.delete(oldest);
27
+ }
28
+ cache.set(key, { value, at: now });
29
+ }
30
+ export function clearJsonCache() {
31
+ cache.clear();
32
+ }
33
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/retrieval/cache.ts"],"names":[],"mappings":"AAKA,MAAM,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAC;AAC5C,MAAM,cAAc,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AACtC,MAAM,WAAW,GAAG,GAAG,CAAC;AAExB;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAC3B,GAAW,EACX,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,EAChB,KAAK,GAAG,cAAc;IAEtB,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,IAAI,GAAG,GAAG,KAAK,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC;QAC3B,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAClB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,wEAAwE;IACxE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAClB,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACtB,OAAO,KAAK,CAAC,KAAU,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,GAAW,EAAE,KAAc,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;IACzE,IAAI,KAAK,CAAC,IAAI,IAAI,WAAW,EAAE,CAAC;QAC9B,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;QACzC,IAAI,MAAM,KAAK,SAAS;YAAE,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACjD,CAAC;IACD,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,KAAK,CAAC,KAAK,EAAE,CAAC;AAChB,CAAC"}
@@ -16,7 +16,7 @@ export interface Candidate {
16
16
  description?: string;
17
17
  thumbnailUrl?: string;
18
18
  /** Where this candidate came from, for provenance and source-typing. */
19
- origin: 'openalex' | 'crossref' | 'itunes' | 'openlibrary' | 'youtube' | 'searxng';
19
+ origin: 'openalex' | 'crossref' | 'semantic-scholar' | 'europepmc' | 'arxiv' | 'itunes' | 'openlibrary' | 'youtube' | 'searxng' | 'brave' | 'serper';
20
20
  /** True when the origin is an academic database, so existence is already attested. */
21
21
  databaseAttested: boolean;
22
22
  }
@@ -1,11 +1,35 @@
1
+ import { getCachedJson, setCachedJson } from './cache.js';
2
+ import { defaultBreaker } from './breaker.js';
1
3
  const FETCH_TIMEOUT_MS = 8000;
2
4
  export async function fetchJson(url, headers = {}) {
3
- const res = await fetch(url, {
4
- headers: { accept: 'application/json', ...headers },
5
- signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
6
- });
7
- if (!res.ok)
8
- throw new Error(`HTTP ${res.status} from ${new URL(url).host}`);
9
- return res.json();
5
+ const cached = getCachedJson(url);
6
+ if (cached !== undefined)
7
+ return cached;
8
+ let host = 'unknown';
9
+ try {
10
+ host = new URL(url).host;
11
+ }
12
+ catch {
13
+ // Let fetch() report the malformed URL below.
14
+ }
15
+ if (!defaultBreaker.canRequest(host)) {
16
+ throw new Error(`Skipping ${host}: recent failures (circuit open)`);
17
+ }
18
+ try {
19
+ const res = await fetch(url, {
20
+ headers: { accept: 'application/json', ...headers },
21
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
22
+ });
23
+ if (!res.ok)
24
+ throw new Error(`HTTP ${res.status} from ${host}`);
25
+ const data = await res.json();
26
+ setCachedJson(url, data);
27
+ defaultBreaker.recordSuccess(host);
28
+ return data;
29
+ }
30
+ catch (error) {
31
+ defaultBreaker.recordFailure(host);
32
+ throw error;
33
+ }
10
34
  }
11
35
  //# sourceMappingURL=candidates.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"candidates.js","sourceRoot":"","sources":["../../src/retrieval/candidates.ts"],"names":[],"mappings":"AAwBA,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAE9B,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,GAAW,EAAE,UAAkC,EAAE;IAC/E,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAC3B,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE,GAAG,OAAO,EAAE;QACnD,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,gBAAgB,CAAC;KAC9C,CAAC,CAAC;IACH,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,GAAG,CAAC,MAAM,SAAS,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7E,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC;AACpB,CAAC"}
1
+ {"version":3,"file":"candidates.js","sourceRoot":"","sources":["../../src/retrieval/candidates.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAmC9C,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAE9B,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,GAAW,EAAE,UAAkC,EAAE;IAC/E,MAAM,MAAM,GAAG,aAAa,CAAU,GAAG,CAAC,CAAC;IAC3C,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IAExC,IAAI,IAAI,GAAG,SAAS,CAAC;IACrB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,8CAA8C;IAChD,CAAC;IACD,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CAAC,YAAY,IAAI,kCAAkC,CAAC,CAAC;IACtE,CAAC;IAED,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAC3B,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE,GAAG,OAAO,EAAE;YACnD,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,gBAAgB,CAAC;SAC9C,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,GAAG,CAAC,MAAM,SAAS,IAAI,EAAE,CAAC,CAAC;QAChE,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAC9B,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACzB,cAAc,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;QACnC,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,cAAc,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;QACnC,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
File without changes
@@ -38,6 +38,7 @@ async function llmCounterQueries(topic, config) {
38
38
  `credible material which DISAGREES with, criticises, or complicates ` +
39
39
  `this topic's usual framing. One query per line, queries only.`,
40
40
  stream: false,
41
+ think: false,
41
42
  options: { num_predict: 60, temperature: 0.4 },
42
43
  }),
43
44
  signal: AbortSignal.timeout(QUERY_TIMEOUT_MS),
@@ -1 +1 @@
1
- {"version":3,"file":"counterpoint.js","sourceRoot":"","sources":["../../src/retrieval/counterpoint.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEhC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,KAAa,EACb,MAAsB;IAEtB,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACrB,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,iBAAiB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YACvD,IAAI,OAAO,CAAC,MAAM;gBAAE,OAAO,OAAO,CAAC;QACrC,CAAC;QAAC,MAAM,CAAC;YACP,gCAAgC;QAClC,CAAC;IACH,CAAC;IACD,OAAO,uBAAuB,CAAC,KAAK,CAAC,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,KAAa;IACnD,MAAM,OAAO,GAAG,cAAc,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IACzC,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IACjE,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,YAAY,EAAE,GAAG,CAAC,uBAAuB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC9F,CAAC;AAED,KAAK,UAAU,iBAAiB,CAAC,KAAa,EAAE,MAAsB;IACpE,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,MAAM,CAAC,SAAU,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,eAAe,EAAE;QAC9E,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,cAAc,EAAE,kBAAkB;YAClC,GAAG,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,UAAU,MAAM,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACnF;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,KAAK,EAAE,MAAM,CAAC,WAAW,IAAI,aAAa;YAC1C,MAAM,EACJ,4BAA4B,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO;gBACvD,gEAAgE;gBAChE,qEAAqE;gBACrE,+DAA+D;YACjE,MAAM,EAAE,KAAK;YACb,OAAO,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE;SAC/C,CAAC;QACF,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,gBAAgB,CAAC;KAC9C,CAAC,CAAC;IACH,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,eAAe,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;IAC1D,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA0B,CAAC;IACzD,OAAO,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC;SACzB,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,uBAAuB,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;SAC/D,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;SACrD,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"counterpoint.js","sourceRoot":"","sources":["../../src/retrieval/counterpoint.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEhC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,KAAa,EACb,MAAsB;IAEtB,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACrB,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,iBAAiB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YACvD,IAAI,OAAO,CAAC,MAAM;gBAAE,OAAO,OAAO,CAAC;QACrC,CAAC;QAAC,MAAM,CAAC;YACP,gCAAgC;QAClC,CAAC;IACH,CAAC;IACD,OAAO,uBAAuB,CAAC,KAAK,CAAC,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,KAAa;IACnD,MAAM,OAAO,GAAG,cAAc,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IACzC,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IACjE,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,YAAY,EAAE,GAAG,CAAC,uBAAuB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC9F,CAAC;AAED,KAAK,UAAU,iBAAiB,CAAC,KAAa,EAAE,MAAsB;IACpE,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,MAAM,CAAC,SAAU,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,eAAe,EAAE;QAC9E,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,cAAc,EAAE,kBAAkB;YAClC,GAAG,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,UAAU,MAAM,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACnF;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,KAAK,EAAE,MAAM,CAAC,WAAW,IAAI,aAAa;YAC1C,MAAM,EACJ,4BAA4B,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO;gBACvD,gEAAgE;gBAChE,qEAAqE;gBACrE,+DAA+D;YACjE,MAAM,EAAE,KAAK;YACb,KAAK,EAAE,KAAK;YACZ,OAAO,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE;SAC/C,CAAC;QACF,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,gBAAgB,CAAC;KAC9C,CAAC,CAAC;IACH,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,eAAe,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;IAC1D,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA0B,CAAC;IACzD,OAAO,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC;SACzB,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,uBAAuB,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;SAC/D,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;SACrD,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACjB,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { type Candidate } from './candidates.js';
2
+ interface EpmcAuthor {
3
+ fullName?: string;
4
+ lastName?: string;
5
+ firstName?: string;
6
+ collectiveName?: string;
7
+ }
8
+ interface EpmcResult {
9
+ id?: string;
10
+ source?: string;
11
+ title?: string;
12
+ doi?: string;
13
+ pubYear?: string;
14
+ authorList?: {
15
+ author?: EpmcAuthor[];
16
+ };
17
+ journalInfo?: {
18
+ journal?: {
19
+ title?: string;
20
+ };
21
+ };
22
+ abstractText?: string;
23
+ isOpenAccess?: string;
24
+ }
25
+ /**
26
+ * Europe PMC (ADR-0013): strong biomedical coverage and keyless. Used as a
27
+ * discovery source, not just a verification lookup, so the query is the topic
28
+ * rather than an exact title.
29
+ */
30
+ export declare function searchEuropePmc(topic: string): Promise<Candidate[]>;
31
+ export declare function parseEuropePmcResults(results: EpmcResult[]): Candidate[];
32
+ export {};
@@ -0,0 +1,62 @@
1
+ import { fetchJson } from './candidates.js';
2
+ import { normalizeDoi } from './paperMerge.js';
3
+ import { throttle } from './rateLimit.js';
4
+ const ENDPOINT = 'https://www.ebi.ac.uk/europepmc/webservices/rest/search';
5
+ const SERVICE = 'europepmc';
6
+ const MIN_INTERVAL_MS = 1000;
7
+ /**
8
+ * Europe PMC (ADR-0013): strong biomedical coverage and keyless. Used as a
9
+ * discovery source, not just a verification lookup, so the query is the topic
10
+ * rather than an exact title.
11
+ */
12
+ export async function searchEuropePmc(topic) {
13
+ await throttle(SERVICE, MIN_INTERVAL_MS);
14
+ const params = new URLSearchParams({
15
+ query: topic,
16
+ format: 'json',
17
+ resultType: 'core',
18
+ pageSize: '8',
19
+ });
20
+ const data = (await fetchJson(`${ENDPOINT}?${params}`, {
21
+ 'user-agent': 'Honeyguide/1.0',
22
+ }));
23
+ return parseEuropePmcResults(data.resultList?.result ?? []);
24
+ }
25
+ export function parseEuropePmcResults(results) {
26
+ return results.flatMap((r) => {
27
+ const title = clean(r.title);
28
+ if (!title)
29
+ return [];
30
+ const doi = normalizeDoi(r.doi);
31
+ const url = (doi ? `https://doi.org/${doi}` : undefined) ??
32
+ (r.id && r.source
33
+ ? `https://europepmc.org/article/${encodeURIComponent(r.source)}/${encodeURIComponent(r.id)}`
34
+ : undefined);
35
+ if (!url)
36
+ return [];
37
+ return [
38
+ {
39
+ title,
40
+ url,
41
+ format: 'paper',
42
+ authors: (r.authorList?.author ?? [])
43
+ .map((a) => a.collectiveName ||
44
+ [a.lastName, a.firstName].filter(Boolean).join(', ') ||
45
+ a.fullName ||
46
+ '')
47
+ .filter(Boolean),
48
+ year: /^\d{4}$/.test(r.pubYear ?? '') ? Number(r.pubYear) : null,
49
+ doi,
50
+ description: r.abstractText ? clean(r.abstractText).slice(0, 600) : undefined,
51
+ venue: r.journalInfo?.journal?.title,
52
+ ...(r.isOpenAccess === 'Y' ? { licensing: 'link-only' } : {}),
53
+ origin: 'europepmc',
54
+ databaseAttested: true,
55
+ },
56
+ ];
57
+ });
58
+ }
59
+ function clean(s) {
60
+ return (s ?? '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
61
+ }
62
+ //# sourceMappingURL=europePmc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"europePmc.js","sourceRoot":"","sources":["../../src/retrieval/europePmc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAkB,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,MAAM,QAAQ,GAAG,yDAAyD,CAAC;AAC3E,MAAM,OAAO,GAAG,WAAW,CAAC;AAC5B,MAAM,eAAe,GAAG,IAAI,CAAC;AAqB7B;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,KAAa;IACjD,MAAM,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;QACjC,KAAK,EAAE,KAAK;QACZ,MAAM,EAAE,MAAM;QACd,UAAU,EAAE,MAAM;QAClB,QAAQ,EAAE,GAAG;KACd,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,CAAC,MAAM,SAAS,CAAC,GAAG,QAAQ,IAAI,MAAM,EAAE,EAAE;QACrD,YAAY,EAAE,gBAAgB;KAC/B,CAAC,CAA+C,CAAC;IAClD,OAAO,qBAAqB,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,IAAI,EAAE,CAAC,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,OAAqB;IACzD,OAAO,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAe,EAAE;QACxC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAC7B,IAAI,CAAC,KAAK;YAAE,OAAO,EAAE,CAAC;QACtB,MAAM,GAAG,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAChC,MAAM,GAAG,GACP,CAAC,GAAG,CAAC,CAAC,CAAC,mBAAmB,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;YAC5C,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,MAAM;gBACf,CAAC,CAAC,iCAAiC,kBAAkB,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,kBAAkB,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE;gBAC7F,CAAC,CAAC,SAAS,CAAC,CAAC;QACjB,IAAI,CAAC,GAAG;YAAE,OAAO,EAAE,CAAC;QACpB,OAAO;YACL;gBACE,KAAK;gBACL,GAAG;gBACH,MAAM,EAAE,OAAO;gBACf,OAAO,EAAE,CAAC,CAAC,CAAC,UAAU,EAAE,MAAM,IAAI,EAAE,CAAC;qBAClC,GAAG,CACF,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,CAAC,cAAc;oBAChB,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;oBACpD,CAAC,CAAC,QAAQ;oBACV,EAAE,CACL;qBACA,MAAM,CAAC,OAAO,CAAC;gBAClB,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI;gBAChE,GAAG;gBACH,WAAW,EAAE,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS;gBAC7E,KAAK,EAAE,CAAC,CAAC,WAAW,EAAE,OAAO,EAAE,KAAK;gBACpC,GAAG,CAAC,CAAC,CAAC,YAAY,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,WAAoB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACtE,MAAM,EAAE,WAAW;gBACnB,gBAAgB,EAAE,IAAI;aACvB;SACF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,KAAK,CAAC,CAAU;IACvB,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AACxE,CAAC"}
File without changes
File without changes
File without changes
@@ -0,0 +1,16 @@
1
+ import type { Candidate } from './candidates.js';
2
+ /**
3
+ * Canonical DOI form for matching: no resolver prefix, no `doi:` label,
4
+ * lower-case. Everything downstream compares on this.
5
+ */
6
+ export declare function normalizeDoi(doi?: string | null): string | undefined;
7
+ /** Title key for matching when a DOI is unavailable. */
8
+ export declare function normalizeTitle(title: string): string;
9
+ /**
10
+ * Merge paper candidates from several databases (ADR-0013). Duplicates are
11
+ * matched by normalised DOI first, then by normalised title, so a DOI-less
12
+ * arXiv record and a Crossref record for the same paper collapse into one.
13
+ * The first occurrence wins for conflicting fields; missing fields are filled
14
+ * from later duplicates.
15
+ */
16
+ export declare function mergePaperCandidates(lists: Candidate[][]): Candidate[];