@inflexa-ai/harness 0.16.0 → 0.18.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 (295) hide show
  1. package/dist/agents/conversation-agent.d.ts +3 -0
  2. package/dist/agents/conversation-agent.js +5 -2
  3. package/dist/agents/report-builder.d.ts +2 -0
  4. package/dist/agents/report-builder.js +2 -0
  5. package/dist/agents/sandbox/shared.d.ts +3 -0
  6. package/dist/agents/sandbox/shared.js +5 -0
  7. package/dist/agents/sandbox/types.d.ts +1 -1
  8. package/dist/app/chat-turn.d.ts +2 -0
  9. package/dist/app/chat-turn.js +11 -2
  10. package/dist/app/spawn-report-session.d.ts +85 -0
  11. package/dist/app/spawn-report-session.js +83 -0
  12. package/dist/app/synthesize-run.d.ts +3 -0
  13. package/dist/app/synthesize-run.js +2 -1
  14. package/dist/citations/__fixtures__/resolver.d.ts +3 -0
  15. package/dist/citations/__fixtures__/resolver.js +9 -0
  16. package/dist/citations/aggregate.d.ts +3 -0
  17. package/dist/citations/aggregate.js +44 -0
  18. package/dist/citations/clients/arxiv.d.ts +6 -0
  19. package/dist/citations/clients/arxiv.js +34 -0
  20. package/dist/citations/clients/common.d.ts +11 -0
  21. package/dist/citations/clients/common.js +43 -0
  22. package/dist/citations/clients/crossref.d.ts +8 -0
  23. package/dist/citations/clients/crossref.js +98 -0
  24. package/dist/citations/clients/doi-registry.d.ts +3 -0
  25. package/dist/citations/clients/doi-registry.js +95 -0
  26. package/dist/citations/clients/pubmed.d.ts +6 -0
  27. package/dist/citations/clients/pubmed.js +83 -0
  28. package/dist/citations/clients/semantic-scholar.d.ts +6 -0
  29. package/dist/citations/clients/semantic-scholar.js +60 -0
  30. package/dist/citations/compare.d.ts +9 -0
  31. package/dist/citations/compare.js +72 -0
  32. package/dist/citations/match.d.ts +11 -0
  33. package/dist/citations/match.js +173 -0
  34. package/dist/citations/normalize.d.ts +31 -0
  35. package/dist/citations/normalize.js +187 -0
  36. package/dist/citations/plan.d.ts +2 -0
  37. package/dist/citations/plan.js +59 -0
  38. package/dist/citations/resolve.d.ts +28 -0
  39. package/dist/citations/resolve.js +238 -0
  40. package/dist/citations/types.d.ts +583 -0
  41. package/dist/citations/types.js +129 -0
  42. package/dist/contracts/chat-events.d.ts +9 -0
  43. package/dist/contracts/index.d.ts +6 -2
  44. package/dist/contracts/index.js +3 -1
  45. package/dist/contracts/organ-system.d.ts +49 -0
  46. package/dist/contracts/organ-system.js +67 -0
  47. package/dist/contracts/report-blocks.d.ts +564 -0
  48. package/dist/contracts/report-blocks.js +117 -0
  49. package/dist/contracts/report-reference.d.ts +459 -0
  50. package/dist/contracts/report-reference.js +256 -0
  51. package/dist/contracts/schemas/chat-events.d.ts +2 -0
  52. package/dist/contracts/schemas/chat-events.js +2 -0
  53. package/dist/contracts/schemas/chat-parts.d.ts +8 -8
  54. package/dist/contracts/severity.d.ts +15 -0
  55. package/dist/contracts/severity.js +10 -0
  56. package/dist/contracts/target-dossier.d.ts +8673 -12970
  57. package/dist/contracts/target-dossier.js +534 -591
  58. package/dist/data/safety-panel-schema.d.ts +18 -5
  59. package/dist/data/safety-panel-schema.js +4 -4
  60. package/dist/execution/artifact-metadata.js +1 -0
  61. package/dist/execution/report-runner.js +1 -3
  62. package/dist/execution/run-synthesis.d.ts +3 -0
  63. package/dist/execution/run-synthesis.js +3 -0
  64. package/dist/index.d.ts +12 -1
  65. package/dist/index.js +22 -0
  66. package/dist/lib/async-utils.d.ts +16 -4
  67. package/dist/lib/async-utils.js +20 -15
  68. package/dist/lib/cache.d.ts +8 -0
  69. package/dist/lib/cache.js +35 -0
  70. package/dist/lib/host-concurrency.js +1 -0
  71. package/dist/lib/rate-limit.d.ts +35 -0
  72. package/dist/lib/rate-limit.js +121 -0
  73. package/dist/literature/sources/arxiv.d.ts +22 -0
  74. package/dist/literature/sources/arxiv.js +103 -0
  75. package/dist/literature/sources/http.d.ts +28 -0
  76. package/dist/literature/sources/http.js +97 -0
  77. package/dist/literature/sources/pubmed.d.ts +68 -0
  78. package/dist/literature/sources/pubmed.js +266 -0
  79. package/dist/literature/sources/semantic-scholar.d.ts +24 -0
  80. package/dist/literature/sources/semantic-scholar.js +71 -0
  81. package/dist/loop/__fixtures__/scripted-provider.js +23 -2
  82. package/dist/loop/run-agent.js +58 -10
  83. package/dist/loop/tool-detail.d.ts +40 -0
  84. package/dist/loop/tool-detail.js +35 -3
  85. package/dist/loop/types.d.ts +13 -0
  86. package/dist/memory/thread-history.d.ts +10 -0
  87. package/dist/memory/thread-history.js +12 -1
  88. package/dist/memory/thread-store.d.ts +142 -25
  89. package/dist/memory/thread-store.js +228 -40
  90. package/dist/prompts/literature-reviewer.d.ts +1 -1
  91. package/dist/prompts/literature-reviewer.js +2 -0
  92. package/dist/prompts/target-assessment/briefs/executive-recommendation.d.ts +1 -1
  93. package/dist/prompts/target-assessment/briefs/executive-recommendation.js +5 -5
  94. package/dist/prompts/target-assessment/briefs/liability-bullets.d.ts +1 -1
  95. package/dist/prompts/target-assessment/briefs/liability-bullets.js +1 -1
  96. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.d.ts +1 -1
  97. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.js +11 -3
  98. package/dist/prompts/target-assessment/investigation/adversarial-critique.d.ts +1 -0
  99. package/dist/prompts/target-assessment/investigation/adversarial-critique.js +67 -0
  100. package/dist/prompts/target-assessment/investigation/claim-reverification.d.ts +1 -0
  101. package/dist/prompts/target-assessment/investigation/claim-reverification.js +49 -0
  102. package/dist/prompts/target-assessment/investigation/index.d.ts +3 -0
  103. package/dist/prompts/target-assessment/investigation/index.js +3 -0
  104. package/dist/prompts/target-assessment/investigation/mechanism-proposal.d.ts +1 -0
  105. package/dist/prompts/target-assessment/investigation/mechanism-proposal.js +49 -0
  106. package/dist/prompts/target-assessment/tox-voice/index.d.ts +1 -1
  107. package/dist/prompts/target-assessment/tox-voice/vocabulary.d.ts +8 -5
  108. package/dist/prompts/target-assessment/tox-voice/vocabulary.js +9 -20
  109. package/dist/providers/__fixtures__/session.d.ts +5 -4
  110. package/dist/providers/__fixtures__/session.js +4 -4
  111. package/dist/report-model/block-walk.d.ts +56 -0
  112. package/dist/report-model/block-walk.js +89 -0
  113. package/dist/report-model/draft-finish.d.ts +60 -0
  114. package/dist/report-model/draft-finish.js +56 -0
  115. package/dist/report-model/draft-operations.d.ts +125 -0
  116. package/dist/report-model/draft-operations.js +341 -0
  117. package/dist/report-model/draft-read.d.ts +59 -0
  118. package/dist/report-model/draft-read.js +109 -0
  119. package/dist/report-model/draft.d.ts +51 -0
  120. package/dist/report-model/draft.js +32 -0
  121. package/dist/report-model/fixture-resolver.d.ts +8 -0
  122. package/dist/report-model/fixture-resolver.js +252 -0
  123. package/dist/report-model/mint-snapshot.d.ts +30 -0
  124. package/dist/report-model/mint-snapshot.js +40 -0
  125. package/dist/report-model/reference-resolver.d.ts +113 -0
  126. package/dist/report-model/reference-resolver.js +60 -0
  127. package/dist/report-model/structural-validation.d.ts +22 -0
  128. package/dist/report-model/structural-validation.js +68 -0
  129. package/dist/report-model/validate.d.ts +40 -0
  130. package/dist/report-model/validate.js +80 -0
  131. package/dist/report-render/chart.d.ts +33 -0
  132. package/dist/report-render/chart.js +538 -0
  133. package/dist/report-render/page.d.ts +127 -0
  134. package/dist/report-render/page.js +307 -0
  135. package/dist/report-render/references.d.ts +32 -0
  136. package/dist/report-render/references.js +44 -0
  137. package/dist/report-render/render.d.ts +25 -0
  138. package/dist/report-render/render.js +125 -0
  139. package/dist/report-render/script-json.d.ts +13 -0
  140. package/dist/report-render/script-json.js +15 -0
  141. package/dist/report-render/types.d.ts +39 -0
  142. package/dist/report-render/types.js +10 -0
  143. package/dist/report-render/views/chart-view.d.ts +11 -0
  144. package/dist/report-render/views/chart-view.js +23 -0
  145. package/dist/report-render/views/page-view.d.ts +20 -0
  146. package/dist/report-render/views/page-view.js +49 -0
  147. package/dist/report-render/views/prose.d.ts +28 -0
  148. package/dist/report-render/views/prose.js +75 -0
  149. package/dist/report-render/views/references-view.d.ts +20 -0
  150. package/dist/report-render/views/references-view.js +64 -0
  151. package/dist/report-render/views/values.d.ts +42 -0
  152. package/dist/report-render/views/values.js +43 -0
  153. package/dist/runtime/assemble.d.ts +50 -4
  154. package/dist/runtime/assemble.js +47 -3
  155. package/dist/runtime/boot.d.ts +3 -3
  156. package/dist/runtime/boot.js +22 -5
  157. package/dist/runtime/dbos.d.ts +4 -4
  158. package/dist/state/artifacts.d.ts +16 -0
  159. package/dist/state/artifacts.js +18 -0
  160. package/dist/state/init.js +77 -1
  161. package/dist/state/purge-analysis.js +1 -0
  162. package/dist/state/report-versions.d.ts +132 -0
  163. package/dist/state/report-versions.js +194 -0
  164. package/dist/state/schema.d.ts +2 -2
  165. package/dist/tasks/data-profile.js +1 -0
  166. package/dist/tools/bio/chembl.js +1 -0
  167. package/dist/tools/bio/comptox.js +1 -0
  168. package/dist/tools/bio/drug-gene-interactions.js +1 -0
  169. package/dist/tools/bio/gene-disease-evidence.js +1 -0
  170. package/dist/tools/bio/gene-preclinical-profile.d.ts +1 -1
  171. package/dist/tools/bio/gene-preclinical-profile.js +1 -0
  172. package/dist/tools/bio/keys.d.ts +4 -2
  173. package/dist/tools/bio/keys.js +3 -2
  174. package/dist/tools/bio/lookup-annotation.js +1 -0
  175. package/dist/tools/bio/opentargets.d.ts +3 -3
  176. package/dist/tools/bio/opentargets.js +4 -3
  177. package/dist/tools/bio/pubchem.js +1 -0
  178. package/dist/tools/bio/pubmed.js +4 -4
  179. package/dist/tools/bio/search-clinical-trials.js +1 -0
  180. package/dist/tools/bio/search-faers.js +1 -0
  181. package/dist/tools/bio/search-geo-datasets.js +1 -0
  182. package/dist/tools/bio/search-interactions.js +1 -0
  183. package/dist/tools/bio/target-safety.d.ts +4 -6
  184. package/dist/tools/bio/target-safety.js +7 -6
  185. package/dist/tools/define-tool.d.ts +19 -12
  186. package/dist/tools/define-tool.js +4 -2
  187. package/dist/tools/display/show-user.js +1 -0
  188. package/dist/tools/iterate-report.js +2 -0
  189. package/dist/tools/lib/chembl-client.d.ts +2 -0
  190. package/dist/tools/lib/chembl-client.js +1 -0
  191. package/dist/tools/lib/monarch-client.d.ts +42 -0
  192. package/dist/tools/lib/monarch-client.js +100 -0
  193. package/dist/tools/lib/ncbi-utils.d.ts +3 -58
  194. package/dist/tools/lib/ncbi-utils.js +2 -286
  195. package/dist/tools/lib/pubmed-client.d.ts +5 -12
  196. package/dist/tools/lib/pubmed-client.js +20 -79
  197. package/dist/tools/report/build-report.js +1 -0
  198. package/dist/tools/report/mint-preview-url.js +1 -0
  199. package/dist/tools/report/preview-snapshot.js +1 -0
  200. package/dist/tools/report/submit-report.js +1 -0
  201. package/dist/tools/report/version-fs.js +4 -0
  202. package/dist/tools/report-authoring/authoring-tools.d.ts +134 -0
  203. package/dist/tools/report-authoring/authoring-tools.js +352 -0
  204. package/dist/tools/research/context7-docs.js +2 -0
  205. package/dist/tools/research/generate-analogy-report.js +3 -2
  206. package/dist/tools/research/generate-plan.js +3 -0
  207. package/dist/tools/research/index.d.ts +1 -0
  208. package/dist/tools/research/index.js +1 -0
  209. package/dist/tools/research/inspect-data-profile.js +4 -0
  210. package/dist/tools/research/inspect-run.d.ts +1 -1
  211. package/dist/tools/research/literature-reviewer.d.ts +3 -0
  212. package/dist/tools/research/literature-reviewer.js +3 -0
  213. package/dist/tools/research/resolve-citation.d.ts +3 -0
  214. package/dist/tools/research/resolve-citation.js +15 -0
  215. package/dist/tools/research/search-arxiv.d.ts +3 -19
  216. package/dist/tools/research/search-arxiv.js +10 -105
  217. package/dist/tools/research/search-github-repos.js +1 -0
  218. package/dist/tools/research/search-semantic-scholar.d.ts +8 -33
  219. package/dist/tools/research/search-semantic-scholar.js +30 -88
  220. package/dist/tools/sandbox/list-available-packages.js +23 -0
  221. package/dist/tools/sandbox/list-available-refs.js +37 -2
  222. package/dist/tools/sandbox/report-blocker.js +1 -0
  223. package/dist/tools/sandbox/skills.js +2 -0
  224. package/dist/tools/workspace/file-stat.js +1 -0
  225. package/dist/tools/workspace/show-file.js +2 -0
  226. package/dist/tools/workspace/show-plan.js +1 -0
  227. package/dist/workflows/execute-analysis.d.ts +8 -1
  228. package/dist/workflows/execute-analysis.js +6 -5
  229. package/dist/workflows/execute-target-assessment.d.ts +13 -2
  230. package/dist/workflows/execute-target-assessment.js +78 -16
  231. package/dist/workflows/sandbox-step.d.ts +5 -0
  232. package/dist/workflows/sandbox-step.js +1 -0
  233. package/dist/workflows/target-assessment/assemblers/index.d.ts +1 -1
  234. package/dist/workflows/target-assessment/assemblers/index.js +1 -1
  235. package/dist/workflows/target-assessment/assemblers/literature.d.ts +9 -27
  236. package/dist/workflows/target-assessment/assemblers/literature.js +32 -48
  237. package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +17 -9
  238. package/dist/workflows/target-assessment/assemblers/orchestrator.js +195 -85
  239. package/dist/workflows/target-assessment/assemblers/safety.d.ts +70 -48
  240. package/dist/workflows/target-assessment/assemblers/safety.js +125 -42
  241. package/dist/workflows/target-assessment/collectors/index.d.ts +5 -2
  242. package/dist/workflows/target-assessment/collectors/index.js +48 -5
  243. package/dist/workflows/target-assessment/coverage.d.ts +43 -27
  244. package/dist/workflows/target-assessment/coverage.js +33 -29
  245. package/dist/workflows/target-assessment/fanout/index.js +1 -0
  246. package/dist/workflows/target-assessment/investigation/index.d.ts +97 -0
  247. package/dist/workflows/target-assessment/investigation/index.js +419 -0
  248. package/dist/workflows/target-assessment/lib/approval-precedents.d.ts +26 -5
  249. package/dist/workflows/target-assessment/lib/approval-precedents.js +95 -29
  250. package/dist/workflows/target-assessment/lib/citation-validator.js +1 -1
  251. package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.d.ts +2 -2
  252. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.d.ts +2 -3
  253. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.js +0 -4
  254. package/dist/workflows/target-assessment/lib/compute-derived.d.ts +6 -5
  255. package/dist/workflows/target-assessment/lib/compute-derived.js +14 -5
  256. package/dist/workflows/target-assessment/lib/family-complex-supplement.d.ts +5 -6
  257. package/dist/workflows/target-assessment/lib/family-complex-supplement.js +2 -3
  258. package/dist/workflows/target-assessment/lib/fda-label-safety.d.ts +72 -0
  259. package/dist/workflows/target-assessment/lib/fda-label-safety.js +166 -0
  260. package/dist/workflows/target-assessment/lib/heterodimer-filter.d.ts +0 -9
  261. package/dist/workflows/target-assessment/lib/heterodimer-filter.js +0 -10
  262. package/dist/workflows/target-assessment/lib/hpo-organ-map.d.ts +34 -0
  263. package/dist/workflows/target-assessment/lib/hpo-organ-map.js +63 -0
  264. package/dist/workflows/target-assessment/lib/impc-organ-map.d.ts +19 -0
  265. package/dist/workflows/target-assessment/lib/impc-organ-map.js +39 -0
  266. package/dist/workflows/target-assessment/lib/meddra-organ-map.d.ts +24 -14
  267. package/dist/workflows/target-assessment/lib/meddra-organ-map.js +59 -47
  268. package/dist/workflows/target-assessment/lib/recommendation-sanitizer.js +1 -1
  269. package/dist/workflows/target-assessment/lib/regulatory-actions.d.ts +2 -3
  270. package/dist/workflows/target-assessment/lib/regulatory-actions.js +33 -4
  271. package/dist/workflows/target-assessment/lib/safety-corroboration.d.ts +44 -0
  272. package/dist/workflows/target-assessment/lib/safety-corroboration.js +273 -0
  273. package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +5 -7
  274. package/dist/workflows/target-assessment/lib/target-identity-filter.js +5 -7
  275. package/dist/workflows/target-assessment/metrics.d.ts +1 -6
  276. package/dist/workflows/target-assessment/metrics.js +1 -13
  277. package/dist/workflows/target-assessment/phase4-assemble.d.ts +2149 -809
  278. package/dist/workflows/target-assessment/phase4-assemble.js +19 -19
  279. package/dist/workflows/target-assessment/phase5-persist.d.ts +20 -21
  280. package/dist/workflows/target-assessment/phase5-persist.js +56 -173
  281. package/dist/workflows/target-assessment/schemas.d.ts +48 -3
  282. package/dist/workflows/target-assessment/schemas.js +19 -0
  283. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.d.ts +1 -0
  284. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.js +2 -0
  285. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +27 -63
  286. package/dist/workflows/target-assessment/steps/phase2-aggregate.js +0 -4
  287. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +28 -155
  288. package/dist/workflows/target-assessment/steps/phase3-aggregate.js +0 -6
  289. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.d.ts +0 -6
  290. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.js +0 -18
  291. package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.d.ts +1 -0
  292. package/dist/workflows/target-assessment/synthesis/index.d.ts +52 -5
  293. package/dist/workflows/target-assessment/synthesis/index.js +4 -1
  294. package/dist/workflows/target-assessment/synthesis-probes.d.ts +3 -3
  295. package/package.json +4 -1
@@ -0,0 +1,187 @@
1
+ import { CitationInputSchema } from "./types.js";
2
+ // Each registry defines its own identifier syntax, and each is matched here rather than parsed:
3
+ // a citation string is prose, so the identifier has to be found inside it, not read off a field.
4
+ // The DOI suffix is deliberately permissive — the registry allows nearly any character in it, so
5
+ // there is no delimiter to stop at, and `trimDoiPunctuation` walks back the sentence punctuation
6
+ // and brackets a greedy match swallows. PMID and arXiv anchor instead, having fixed shapes.
7
+ const DOI_PATTERN = /(?:https?:\/\/(?:dx\.)?doi\.org\/|doi\s*:\s*)?(10\.\d{4,9}\/[^\s<>"']+)/i;
8
+ const PMID_PATTERN = /^(?:pmid\s*:\s*)?(\d{1,9})$/i;
9
+ const ARXIV_PATTERN = /^(?:https?:\/\/arxiv\.org\/(?:abs|pdf)\/|arxiv\s*:\s*)?((?:\d{4}\.\d{4,5}|[a-z-]+(?:\.[a-z-]+)?\/\d{7})(?:v\d+)?)(?:\.pdf)?$/i;
10
+ function collapseWhitespace(value) {
11
+ return value.normalize("NFKC").replace(/\s+/g, " ").trim();
12
+ }
13
+ export function normalizeComparable(value) {
14
+ return collapseWhitespace(value)
15
+ .toLocaleLowerCase("en-US")
16
+ .replace(/[\p{P}\p{S}]+/gu, " ")
17
+ .replace(/\s+/g, " ")
18
+ .trim();
19
+ }
20
+ export function normalizeAuthor(value) {
21
+ return normalizeComparable(value)
22
+ .replace(/\b(?:jr|sr|ii|iii|iv)\b/g, "")
23
+ .replace(/\s+/g, " ")
24
+ .trim();
25
+ }
26
+ function authorEvidence(value) {
27
+ const tokens = normalizeAuthor(value).split(" ").filter(Boolean);
28
+ return { words: tokens.filter((token) => token.length > 1), initials: tokens.filter((token) => token.length === 1) };
29
+ }
30
+ /**
31
+ * A token long enough to be a spelled-out name part rather than an unpunctuated
32
+ * block of initials. Two letters is the boundary: `Smith JA` and `Wu` are both
33
+ * live readings of the same shape, so a token that short is never treated as a
34
+ * name part that could contradict one.
35
+ */
36
+ const SPELLED_OUT_MINIMUM = 3;
37
+ /**
38
+ * The given-name letters a name can account for. A short leftover may itself be
39
+ * an initial block, so it contributes every letter; a spelled-out one
40
+ * contributes only the initial it would abbreviate to.
41
+ */
42
+ function givenLetters(leftover, initials) {
43
+ return [...initials, ...leftover.flatMap((word) => (word.length < SPELLED_OUT_MINIMUM ? [...word] : [word[0]]))].sort();
44
+ }
45
+ function containsLetters(available, required) {
46
+ const pool = [...available];
47
+ return required.every((letter) => {
48
+ const index = pool.indexOf(letter);
49
+ if (index < 0)
50
+ return false;
51
+ pool.splice(index, 1);
52
+ return true;
53
+ });
54
+ }
55
+ /**
56
+ * Whether two author strings can name the same person. Written surname-position
57
+ * free, because citation styles disagree about it (`Smith, Jane A.`,
58
+ * `Jane A. Smith`, `Smith JA` all reach us) and normalization has already
59
+ * discarded the comma that would settle it.
60
+ *
61
+ * Sharing a name part is necessary but never sufficient: two spelled-out names
62
+ * that each carry a part the other lacks are different people, and where both
63
+ * sides supply given-name evidence it has to agree. Surname-only agreement is
64
+ * accepted only when one side supplies no given name at all — the case where
65
+ * there is nothing to contradict.
66
+ */
67
+ export function authorNamesMatch(left, right) {
68
+ const a = authorEvidence(left);
69
+ const b = authorEvidence(right);
70
+ if (!a.words.some((word) => b.words.includes(word)))
71
+ return false;
72
+ const leftoverA = a.words.filter((word) => !b.words.includes(word));
73
+ const leftoverB = b.words.filter((word) => !a.words.includes(word));
74
+ const spelledOut = (leftover) => leftover.some((word) => word.length >= SPELLED_OUT_MINIMUM);
75
+ if (spelledOut(leftoverA) && spelledOut(leftoverB))
76
+ return false;
77
+ const lettersA = givenLetters(leftoverA, a.initials);
78
+ const lettersB = givenLetters(leftoverB, b.initials);
79
+ if (lettersA.length === 0 || lettersB.length === 0)
80
+ return true;
81
+ return lettersA.length <= lettersB.length ? containsLetters(lettersB, lettersA) : containsLetters(lettersA, lettersB);
82
+ }
83
+ function trimDoiPunctuation(value) {
84
+ let result = value.replace(/[.,;]+$/g, "");
85
+ const pairs = [
86
+ ["(", ")"],
87
+ ["[", "]"],
88
+ ["{", "}"],
89
+ ];
90
+ for (const [open, close] of pairs) {
91
+ while (result.endsWith(close) && result.split(open).length <= result.split(close).length)
92
+ result = result.slice(0, -1);
93
+ }
94
+ return result;
95
+ }
96
+ export function extractDoi(value) {
97
+ const match = collapseWhitespace(value).match(DOI_PATTERN);
98
+ if (!match?.[1])
99
+ return undefined;
100
+ return trimDoiPunctuation(match[1]).toLocaleLowerCase("en-US");
101
+ }
102
+ export function extractPmid(value) {
103
+ return collapseWhitespace(value).match(PMID_PATTERN)?.[1];
104
+ }
105
+ export function extractArxivId(value) {
106
+ const match = collapseWhitespace(value).match(ARXIV_PATTERN)?.[1];
107
+ return match?.toLocaleLowerCase("en-US").replace(/v\d+$/i, "");
108
+ }
109
+ function classifyUnsupportedWork(value) {
110
+ const normalized = normalizeComparable(value);
111
+ if (/\bpersonal communication\b/.test(normalized))
112
+ return "personal_communication";
113
+ if (/\bunpublished (?:data|work|manuscript)\b/.test(normalized))
114
+ return "unpublished";
115
+ if (/\b(?:in press|forthcoming)\b/.test(normalized))
116
+ return "unregistered_in_press";
117
+ return undefined;
118
+ }
119
+ function normalizedSupplied(input) {
120
+ return {
121
+ ...(input.title === undefined ? {} : { title: normalizeComparable(input.title) }),
122
+ ...(input.authors === undefined ? {} : { authors: input.authors.map(normalizeAuthor).filter(Boolean) }),
123
+ ...(input.year === undefined ? {} : { year: input.year }),
124
+ ...(input.venue === undefined ? {} : { venue: normalizeComparable(input.venue) }),
125
+ ...(input.volume === undefined ? {} : { volume: normalizeComparable(input.volume) }),
126
+ ...(input.firstPage === undefined ? {} : { firstPage: normalizeComparable(input.firstPage) }),
127
+ };
128
+ }
129
+ export function normalizeCitation(unparsed) {
130
+ const input = CitationInputSchema.parse(unparsed);
131
+ const citation = collapseWhitespace(input.citation);
132
+ const doi = extractDoi(citation);
133
+ const pmid = extractPmid(citation);
134
+ const arxiv = extractArxivId(citation);
135
+ const hint = input.kind ?? "auto";
136
+ let kind;
137
+ if (hint === "auto")
138
+ kind = doi ? "doi" : pmid ? "pmid" : arxiv ? "arxiv" : "free_text";
139
+ else
140
+ kind = hint;
141
+ if (kind === "doi" && doi === undefined)
142
+ throw new Error("citation kind is doi but no valid DOI was found");
143
+ if (kind === "pmid" && pmid === undefined)
144
+ throw new Error("citation kind is pmid but no valid PMID was found");
145
+ if (kind === "arxiv" && arxiv === undefined)
146
+ throw new Error("citation kind is arxiv but no valid arXiv id was found");
147
+ const unsupportedWorkKind = kind === "free_text" ? classifyUnsupportedWork(citation) : undefined;
148
+ // A pinned `free_text` kind asserts the citation carries no usable identifier — `auto`
149
+ // would have classified it otherwise. Publishing one anyway splits the pipeline against
150
+ // itself: the plan gives the citation bibliographic-only treatment while candidate
151
+ // scoring short-circuits to a perfect match on an identifier nothing verified.
152
+ const identified = kind !== "free_text";
153
+ return {
154
+ citation,
155
+ query: normalizeComparable(citation),
156
+ kind,
157
+ identifiers: {
158
+ ...(doi === undefined || !identified ? {} : { doi }),
159
+ ...(pmid === undefined || !identified ? {} : { pmid }),
160
+ ...(arxiv === undefined || !identified ? {} : { arxiv }),
161
+ },
162
+ supplied: normalizedSupplied(input),
163
+ ...(unsupportedWorkKind === undefined ? {} : { unsupportedWorkKind }),
164
+ };
165
+ }
166
+ /**
167
+ * Identity of the *lookups* an input provokes, not of the input itself.
168
+ *
169
+ * Two citations share this key when the authorities would be asked the same
170
+ * question, so they share one round of source outcomes; each caller's own
171
+ * supplied metadata is compared against those outcomes separately. An exact
172
+ * identifier is the whole question — every operation an identifier plan selects
173
+ * is driven by the identifier alone — so supplied metadata stays out of the key
174
+ * there. A free-text citation is dispatched from its metadata, and folds it in.
175
+ */
176
+ export function citationLookupKey(input) {
177
+ const normalized = normalizeCitation(input);
178
+ return JSON.stringify(normalized.kind === "free_text"
179
+ ? {
180
+ kind: normalized.kind,
181
+ identifiers: normalized.identifiers,
182
+ query: normalized.query,
183
+ supplied: normalized.supplied,
184
+ unsupportedWorkKind: normalized.unsupportedWorkKind ?? null,
185
+ }
186
+ : { kind: normalized.kind, identifiers: normalized.identifiers });
187
+ }
@@ -0,0 +1,2 @@
1
+ import type { CitationInput, CitationSourcePlanItem, NormalizedCitation } from "./types.js";
2
+ export declare function planCitationSources(input: CitationInput, normalized: NormalizedCitation): CitationSourcePlanItem[];
@@ -0,0 +1,59 @@
1
+ const SOURCES = ["doi_registry", "crossref", "pubmed", "arxiv", "semantic_scholar"];
2
+ function item(source, applicable, operation, reason, candidateGeneration = false) {
3
+ return { source, applicable, operation: applicable ? operation : "none", candidateGeneration: applicable && candidateGeneration, reason };
4
+ }
5
+ function enoughForEcitMatch(input) {
6
+ if (!input.venue)
7
+ return false;
8
+ const disambiguators = [input.year, input.volume, input.firstPage, input.authors?.[0]].filter((value) => value !== undefined).length;
9
+ return disambiguators >= 2;
10
+ }
11
+ function indicatesArxiv(input, normalized) {
12
+ return /\b(?:arxiv|preprint)\b/i.test(input.citation) || /\barxiv\b/i.test(input.venue ?? "") || normalized.identifiers.arxiv !== undefined;
13
+ }
14
+ export function planCitationSources(input, normalized) {
15
+ if (normalized.unsupportedWorkKind !== undefined) {
16
+ return SOURCES.map((source) => item(source, false, "none", `unsupported work kind: ${normalized.unsupportedWorkKind}`));
17
+ }
18
+ if (normalized.kind === "doi") {
19
+ return [
20
+ item("doi_registry", true, "doi_exact", "exact DOI handle and registration-agency lookup"),
21
+ item("crossref", true, "crossref_doi_if_owned", "exact lookup only when DOI registry identifies Crossref"),
22
+ item("pubmed", true, "pubmed_doi", "bounded DOI lookup in PubMed", true),
23
+ item("arxiv", false, "none", "a DOI alone does not indicate an arXiv preprint"),
24
+ item("semantic_scholar", true, "semantic_scholar_identifier", "DOI identifier lookup", true),
25
+ ];
26
+ }
27
+ if (normalized.kind === "pmid") {
28
+ return [
29
+ item("doi_registry", false, "none", "no DOI supplied"),
30
+ item("crossref", false, "none", "PMID-only input has no Crossref bibliographic query"),
31
+ item("pubmed", true, "pubmed_exact", "exact PMID retrieval"),
32
+ item("arxiv", false, "none", "PMID-only input does not indicate arXiv"),
33
+ item("semantic_scholar", true, "semantic_scholar_identifier", "PMID identifier lookup", true),
34
+ ];
35
+ }
36
+ if (normalized.kind === "arxiv") {
37
+ return [
38
+ item("doi_registry", false, "none", "no DOI supplied"),
39
+ item("crossref", false, "none", "arXiv-only input has no Crossref bibliographic query"),
40
+ item("pubmed", false, "none", "arXiv-only input has no PubMed query"),
41
+ item("arxiv", true, "arxiv_exact", "exact arXiv identifier retrieval"),
42
+ item("semantic_scholar", true, "semantic_scholar_identifier", "arXiv identifier lookup", true),
43
+ ];
44
+ }
45
+ const pubmedStructured = enoughForEcitMatch(input);
46
+ const pubmedTitle = !pubmedStructured && input.title !== undefined;
47
+ const arxiv = indicatesArxiv(input, normalized);
48
+ return [
49
+ item("doi_registry", false, "none", "no exact DOI supplied"),
50
+ item("crossref", true, "crossref_bibliographic", "raw or structured bibliographic match", true),
51
+ item("pubmed", pubmedStructured || pubmedTitle, pubmedStructured ? "pubmed_structured" : "pubmed_title", pubmedStructured
52
+ ? "structured fields support constrained citation match"
53
+ : pubmedTitle
54
+ ? "supplied title supports bounded candidate search"
55
+ : "insufficient structured PubMed signal", true),
56
+ item("arxiv", arxiv, "arxiv_bibliographic", arxiv ? "citation indicates an arXiv preprint" : "citation does not indicate arXiv", true),
57
+ item("semantic_scholar", true, "semantic_scholar_bibliographic", "bounded bibliographic candidate search", true),
58
+ ];
59
+ }
@@ -0,0 +1,28 @@
1
+ import { type CitationMatchConfig } from "./match.js";
2
+ import { type RateLimitConfig, type RateLimitRuntime } from "../lib/rate-limit.js";
3
+ import { type CitationResolver, type CitationSource, type CitationSourceClient } from "./types.js";
4
+ export interface CitationResolverSourceConfig extends Partial<RateLimitConfig> {
5
+ readonly enabled?: boolean;
6
+ }
7
+ export interface CitationResolverConfig {
8
+ readonly maxBatchSize?: number;
9
+ readonly timeoutMs?: number;
10
+ readonly cache?: {
11
+ readonly maximum?: number;
12
+ readonly positiveTtlMs?: number;
13
+ readonly negativeTtlMs?: number;
14
+ };
15
+ readonly match?: Partial<CitationMatchConfig>;
16
+ readonly sources?: Partial<Record<CitationSource, CitationResolverSourceConfig>>;
17
+ readonly crossref?: {
18
+ readonly mailto?: string;
19
+ readonly userAgent?: string;
20
+ };
21
+ readonly ncbiApiKey?: string;
22
+ readonly semanticScholarApiKey?: string;
23
+ }
24
+ export interface CitationResolverDependencies extends RateLimitRuntime {
25
+ readonly fetch?: typeof globalThis.fetch;
26
+ readonly clients?: readonly CitationSourceClient[];
27
+ }
28
+ export declare function createCitationResolver(config?: CitationResolverConfig, deps?: CitationResolverDependencies): CitationResolver;
@@ -0,0 +1,238 @@
1
+ import { aggregateCitationResolution } from "./aggregate.js";
2
+ import { BoundedTtlCache } from "../lib/cache.js";
3
+ import { createArxivCitationClient } from "./clients/arxiv.js";
4
+ import { notApplicableOutcome, sourceOutcome } from "./clients/common.js";
5
+ import { createCrossrefClient } from "./clients/crossref.js";
6
+ import { createDoiRegistryClient } from "./clients/doi-registry.js";
7
+ import { createPubmedCitationClient } from "./clients/pubmed.js";
8
+ import { createSemanticScholarCitationClient } from "./clients/semantic-scholar.js";
9
+ import { DEFAULT_MATCH_CONFIG } from "./match.js";
10
+ import { citationLookupKey, normalizeCitation } from "./normalize.js";
11
+ import { planCitationSources } from "./plan.js";
12
+ import { createRateLimitSchedule } from "../lib/rate-limit.js";
13
+ import { CitationInputSchema, } from "./types.js";
14
+ const SOURCE_PLAN_VERSION = "citation-plan-v1";
15
+ const SOURCE_ORDER = ["doi_registry", "crossref", "pubmed", "arxiv", "semantic_scholar"];
16
+ const DEFAULT_RATE_LIMITS = {
17
+ doi_registry: { maxConcurrency: 2, requestsPerSecond: 5, maxRetries: 2, maxRetryDelayMs: 5_000 },
18
+ crossref: { maxConcurrency: 2, requestsPerSecond: 5, maxRetries: 2, maxRetryDelayMs: 5_000 },
19
+ pubmed: { maxConcurrency: 3, requestsPerSecond: 3, maxRetries: 2, maxRetryDelayMs: 5_000 },
20
+ arxiv: { maxConcurrency: 1, requestsPerSecond: 1, maxRetries: 2, maxRetryDelayMs: 10_000 },
21
+ semantic_scholar: { maxConcurrency: 1, requestsPerSecond: 1, maxRetries: 2, maxRetryDelayMs: 10_000 },
22
+ };
23
+ function mergedRateLimit(source, config) {
24
+ return { ...DEFAULT_RATE_LIMITS[source], ...(config.sources?.[source] ?? {}) };
25
+ }
26
+ function createDefaultClients(config, deps) {
27
+ const runtime = { ...(deps.now ? { now: deps.now } : {}), ...(deps.sleep ? { sleep: deps.sleep } : {}) };
28
+ const http = (source) => {
29
+ const limits = mergedRateLimit(source, config);
30
+ return {
31
+ ...(deps.fetch === undefined ? {} : { fetch: deps.fetch }),
32
+ ...(config.timeoutMs === undefined ? {} : { timeoutMs: config.timeoutMs }),
33
+ ...(deps.sleep ? { sleep: deps.sleep } : {}),
34
+ schedule: createRateLimitSchedule(limits, runtime),
35
+ maxRetries: limits.maxRetries,
36
+ maxRetryDelayMs: limits.maxRetryDelayMs,
37
+ };
38
+ };
39
+ return [
40
+ createDoiRegistryClient(http("doi_registry")),
41
+ createCrossrefClient({
42
+ ...http("crossref"),
43
+ ...(config.crossref?.mailto === undefined ? {} : { mailto: config.crossref.mailto }),
44
+ ...(config.crossref?.userAgent === undefined ? {} : { userAgent: config.crossref.userAgent }),
45
+ }),
46
+ createPubmedCitationClient({
47
+ ...http("pubmed"),
48
+ ...(config.ncbiApiKey === undefined ? {} : { apiKey: config.ncbiApiKey }),
49
+ }),
50
+ createArxivCitationClient(http("arxiv")),
51
+ createSemanticScholarCitationClient({
52
+ ...http("semantic_scholar"),
53
+ ...(config.semanticScholarApiKey === undefined ? {} : { apiKey: config.semanticScholarApiKey }),
54
+ }),
55
+ ];
56
+ }
57
+ function cacheable(outcomes) {
58
+ if (outcomes.some((outcome) => outcome.status === "unavailable"))
59
+ return undefined;
60
+ return outcomes.some((outcome) => outcome.records.length > 0) ? "positive" : "negative";
61
+ }
62
+ function abortReason(signal) {
63
+ return signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
64
+ }
65
+ function withCallerSignal(promise, signal) {
66
+ if (signal === undefined)
67
+ return promise;
68
+ if (signal.aborted)
69
+ return Promise.reject(abortReason(signal));
70
+ return new Promise((resolve, reject) => {
71
+ const onAbort = () => reject(abortReason(signal));
72
+ signal.addEventListener("abort", onAbort, { once: true });
73
+ promise.then((value) => {
74
+ signal.removeEventListener("abort", onAbort);
75
+ resolve(value);
76
+ }, (error) => {
77
+ signal.removeEventListener("abort", onAbort);
78
+ reject(error);
79
+ });
80
+ });
81
+ }
82
+ /**
83
+ * Read the DOI registry's outcome as evidence about the registration agency.
84
+ *
85
+ * The registry runs first in `SOURCE_ORDER`, so every later source sees its
86
+ * result. Only a named agency is a finding; every other shape — the registry
87
+ * disabled, unreachable, or silent on the agency — leaves ownership unknown.
88
+ */
89
+ function registrationAgencyEvidence(outcome) {
90
+ if (outcome === undefined)
91
+ return { status: "undetermined", detail: "the DOI registry was not consulted" };
92
+ const evidence = outcome.identifierEvidence;
93
+ if (evidence?.registrationAgency !== undefined)
94
+ return { status: "determined", agency: evidence.registrationAgency };
95
+ if (evidence?.exists === false)
96
+ return { status: "absent" };
97
+ return { status: "undetermined", detail: outcome.detail ?? `the DOI registry returned ${outcome.status}` };
98
+ }
99
+ /**
100
+ * Run one source over the requests routed to it, keeping its failures its own.
101
+ *
102
+ * A source that throws, or that answers a batch with the wrong number of
103
+ * outcomes, is an unavailable source — the same operational state as an HTTP
104
+ * failure — not a failed batch. Caller cancellation still propagates.
105
+ */
106
+ async function runSource(client, requests, signal) {
107
+ const unavailable = (request, detail) => sourceOutcome(client.source, request.plan.operation, "unavailable", 0, [], detail);
108
+ const describe = (error) => (error instanceof Error ? error.message : String(error));
109
+ if (client.resolveMany === undefined) {
110
+ const settled = await Promise.allSettled(requests.map((request) => client.resolve(request, signal)));
111
+ if (signal?.aborted)
112
+ throw abortReason(signal);
113
+ return settled.map((outcome, index) => (outcome.status === "fulfilled" ? outcome.value : unavailable(requests[index], describe(outcome.reason))));
114
+ }
115
+ try {
116
+ const resolved = await client.resolveMany(requests, signal);
117
+ if (resolved.length === requests.length)
118
+ return [...resolved];
119
+ return requests.map((request) => unavailable(request, `${client.source} returned ${resolved.length} outcomes for ${requests.length} requests`));
120
+ }
121
+ catch (error) {
122
+ if (signal?.aborted)
123
+ throw abortReason(signal);
124
+ return requests.map((request) => unavailable(request, describe(error)));
125
+ }
126
+ }
127
+ export function createCitationResolver(config = {}, deps = {}) {
128
+ const clients = new Map((deps.clients ?? createDefaultClients(config, deps)).map((client) => [client.source, client]));
129
+ const now = deps.now ?? Date.now;
130
+ const cacheConfig = {
131
+ maximum: config.cache?.maximum ?? 500,
132
+ positiveTtlMs: config.cache?.positiveTtlMs ?? 15 * 60_000,
133
+ negativeTtlMs: config.cache?.negativeTtlMs ?? 2 * 60_000,
134
+ };
135
+ const cache = new BoundedTtlCache(cacheConfig.maximum, now);
136
+ const inFlight = new Map();
137
+ const matchConfig = { ...DEFAULT_MATCH_CONFIG, ...(config.match ?? {}) };
138
+ const maxBatchSize = config.maxBatchSize ?? 200;
139
+ const keyFor = (input) => `${SOURCE_PLAN_VERSION}:${citationLookupKey(input)}`;
140
+ async function resolveUncachedBatch(entries, signal) {
141
+ const outcomes = entries.map(() => new Map());
142
+ for (const source of SOURCE_ORDER) {
143
+ const sourceConfig = config.sources?.[source];
144
+ const client = clients.get(source);
145
+ const requests = [];
146
+ const requestIndices = [];
147
+ for (let index = 0; index < entries.length; index += 1) {
148
+ const entry = entries[index];
149
+ const plan = entry.plans.find((candidate) => candidate.source === source);
150
+ if (!plan.applicable || sourceConfig?.enabled === false || client === undefined) {
151
+ outcomes[index].set(source, sourceConfig?.enabled === false
152
+ ? notApplicableOutcome({ ...plan, applicable: false, operation: "none", reason: "source disabled by resolver configuration" })
153
+ : client === undefined
154
+ ? notApplicableOutcome({ ...plan, applicable: false, operation: "none", reason: "source client unavailable" })
155
+ : notApplicableOutcome(plan));
156
+ continue;
157
+ }
158
+ requests.push({
159
+ input: entry.input,
160
+ normalized: entry.normalized,
161
+ plan,
162
+ registrationAgency: registrationAgencyEvidence(outcomes[index].get("doi_registry")),
163
+ });
164
+ requestIndices.push(index);
165
+ }
166
+ if (requests.length === 0 || client === undefined)
167
+ continue;
168
+ const resolved = await runSource(client, requests, signal);
169
+ for (let requestIndex = 0; requestIndex < resolved.length; requestIndex += 1) {
170
+ outcomes[requestIndices[requestIndex]].set(source, resolved[requestIndex]);
171
+ }
172
+ }
173
+ return entries.map((entry, index) => SOURCE_ORDER.map((source) => outcomes[index].get(source) ?? notApplicableOutcome(entry.plans.find((plan) => plan.source === source))));
174
+ }
175
+ async function resolveMany(inputs, options = {}) {
176
+ if (inputs.length > maxBatchSize)
177
+ throw new Error(`citation batch size ${inputs.length} exceeds configured maximum ${maxBatchSize}`);
178
+ if (inputs.length === 0)
179
+ return [];
180
+ const parsed = inputs.map((input) => CitationInputSchema.parse(input));
181
+ const uniqueOrder = [];
182
+ const firstByKey = new Map();
183
+ for (const input of parsed) {
184
+ const key = keyFor(input);
185
+ if (!firstByKey.has(key)) {
186
+ uniqueOrder.push(key);
187
+ firstByKey.set(key, input);
188
+ }
189
+ }
190
+ const pending = new Map();
191
+ const uncached = [];
192
+ for (const key of uniqueOrder) {
193
+ const cached = cache.get(key);
194
+ if (cached !== undefined) {
195
+ pending.set(key, Promise.resolve(cached));
196
+ continue;
197
+ }
198
+ const running = inFlight.get(key);
199
+ if (running !== undefined) {
200
+ pending.set(key, running);
201
+ continue;
202
+ }
203
+ const input = firstByKey.get(key);
204
+ const normalized = normalizeCitation(input);
205
+ uncached.push({ key, entry: { input, normalized, plans: planCitationSources(input, normalized) } });
206
+ }
207
+ if (uncached.length > 0) {
208
+ const batch = resolveUncachedBatch(uncached.map(({ entry }) => entry), options.signal);
209
+ for (let index = 0; index < uncached.length; index += 1) {
210
+ const { key } = uncached[index];
211
+ const one = batch.then((results) => results[index]);
212
+ const tracked = one.then((outcomes) => {
213
+ const kind = cacheable(outcomes);
214
+ if (kind !== undefined)
215
+ cache.set(key, outcomes, kind === "positive" ? cacheConfig.positiveTtlMs : cacheConfig.negativeTtlMs);
216
+ inFlight.delete(key);
217
+ return outcomes;
218
+ }, (error) => {
219
+ inFlight.delete(key);
220
+ throw error;
221
+ });
222
+ inFlight.set(key, tracked);
223
+ pending.set(key, tracked);
224
+ }
225
+ }
226
+ const outcomesByKey = new Map();
227
+ await Promise.all(uniqueOrder.map(async (key) => {
228
+ outcomesByKey.set(key, await withCallerSignal(pending.get(key), options.signal));
229
+ }));
230
+ // Aggregated per input, never per shared lookup: comparisons and the
231
+ // verdict they drive belong to the caller's own supplied metadata.
232
+ return parsed.map((input) => aggregateCitationResolution(input, normalizeCitation(input), outcomesByKey.get(keyFor(input)), matchConfig));
233
+ }
234
+ return {
235
+ resolveOne: async (input, options = {}) => (await resolveMany([input], options))[0],
236
+ resolveMany,
237
+ };
238
+ }