@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,98 @@
1
+ import { z } from "zod";
2
+ import { encodeDoiPath, firstNonEmpty, parseYear, sourceOutcome } from "./common.js";
3
+ import { requestJson } from "../../literature/sources/http.js";
4
+ const CrossrefItemSchema = z
5
+ .object({
6
+ DOI: z.string().optional(),
7
+ URL: z.string().optional(),
8
+ title: z.array(z.string()).optional(),
9
+ author: z.array(z.object({ given: z.string().optional(), family: z.string().optional(), name: z.string().optional() }).passthrough()).optional(),
10
+ issued: z.object({ "date-parts": z.array(z.array(z.number())).optional() }).optional(),
11
+ published: z.object({ "date-parts": z.array(z.array(z.number())).optional() }).optional(),
12
+ "container-title": z.array(z.string()).optional(),
13
+ volume: z.string().optional(),
14
+ page: z.string().optional(),
15
+ })
16
+ .passthrough();
17
+ const ExactSchema = z.object({ message: CrossrefItemSchema });
18
+ const SearchSchema = z.object({ message: z.object({ items: z.array(CrossrefItemSchema) }).passthrough() });
19
+ function crossrefRecord(item, index) {
20
+ const doi = item.DOI?.toLocaleLowerCase("en-US");
21
+ const sourceRecordId = firstNonEmpty([doi, item.URL, `candidate-${index}`]);
22
+ if (sourceRecordId === undefined)
23
+ return undefined;
24
+ const title = firstNonEmpty(item.title ?? []);
25
+ const venue = firstNonEmpty(item["container-title"] ?? []);
26
+ const authors = item.author
27
+ ?.map((author) => firstNonEmpty([author.name, [author.given, author.family].filter(Boolean).join(" ")]))
28
+ .filter((author) => author !== undefined);
29
+ const dateParts = item.issued?.["date-parts"]?.[0] ?? item.published?.["date-parts"]?.[0];
30
+ return {
31
+ source: "crossref",
32
+ sourceRecordId,
33
+ identifiers: { ...(doi === undefined ? {} : { doi }) },
34
+ ...(title === undefined ? {} : { title }),
35
+ ...(authors === undefined || authors.length === 0 ? {} : { authors }),
36
+ ...(dateParts?.[0] === undefined ? {} : { year: parseYear(dateParts[0]) }),
37
+ ...(venue === undefined ? {} : { venue }),
38
+ ...(item.volume === undefined ? {} : { volume: item.volume }),
39
+ ...(item.page === undefined ? {} : { firstPage: item.page.split(/[-–]/)[0] }),
40
+ ...(item.URL === undefined ? {} : { url: item.URL }),
41
+ };
42
+ }
43
+ export function createCrossrefClient(options = {}) {
44
+ const headers = {};
45
+ if (options.userAgent)
46
+ headers["User-Agent"] = options.userAgent;
47
+ return {
48
+ source: "crossref",
49
+ async resolve(request, signal) {
50
+ if (!request.plan.applicable)
51
+ return sourceOutcome("crossref", request.plan.operation, "not_applicable", 0, [], request.plan.reason);
52
+ const registration = request.registrationAgency;
53
+ if (request.plan.operation === "crossref_doi_if_owned") {
54
+ // Skipping because ownership lies elsewhere is a finding; skipping because
55
+ // nobody could tell us is a gap, and reporting the gap as `not_applicable`
56
+ // would let an unchecked DOI reach `not_found` on complete coverage.
57
+ if (registration.status === "undetermined") {
58
+ return sourceOutcome("crossref", request.plan.operation, "unavailable", 0, [], `registration agency undetermined: ${registration.detail}`);
59
+ }
60
+ if (registration.status === "absent") {
61
+ return sourceOutcome("crossref", request.plan.operation, "not_applicable", 0, [], "the DOI handle does not resolve");
62
+ }
63
+ if (registration.agency.toLocaleLowerCase("en-US") !== "crossref") {
64
+ return sourceOutcome("crossref", request.plan.operation, "not_applicable", 0, [], "DOI is not registered by Crossref");
65
+ }
66
+ }
67
+ const doi = request.normalized.identifiers.doi;
68
+ const exact = request.plan.operation === "crossref_doi_if_owned";
69
+ const url = exact
70
+ ? (() => {
71
+ const target = new URL(`https://api.crossref.org/works/${encodeDoiPath(doi)}`);
72
+ if (options.mailto)
73
+ target.searchParams.set("mailto", options.mailto);
74
+ return target.toString();
75
+ })()
76
+ : (() => {
77
+ const params = new URLSearchParams({
78
+ "query.bibliographic": request.input.citation,
79
+ rows: String(options.maxCandidates ?? 5),
80
+ });
81
+ if (options.mailto)
82
+ params.set("mailto", options.mailto);
83
+ return `https://api.crossref.org/works?${params}`;
84
+ })();
85
+ const result = exact
86
+ ? await requestJson(url, ExactSchema, options, signal, { headers })
87
+ : await requestJson(url, SearchSchema, options, signal, { headers });
88
+ if (result.status !== "ok")
89
+ return sourceOutcome("crossref", request.plan.operation, result.status, 1, [], result.detail);
90
+ const items = exact ? [result.value.message] : result.value.message.items;
91
+ const records = items.flatMap((item, index) => {
92
+ const record = crossrefRecord(item, index);
93
+ return record === undefined ? [] : [record];
94
+ });
95
+ return sourceOutcome("crossref", request.plan.operation, records.length === 0 ? "no_data" : "ok", 1, records);
96
+ },
97
+ };
98
+ }
@@ -0,0 +1,3 @@
1
+ import type { CitationSourceClient } from "../types.js";
2
+ import { type SourceHttpOptions } from "../../literature/sources/http.js";
3
+ export declare function createDoiRegistryClient(options?: SourceHttpOptions): CitationSourceClient;
@@ -0,0 +1,95 @@
1
+ import { z } from "zod";
2
+ import { encodeDoiPath, firstNonEmpty, parseYear, sourceOutcome } from "./common.js";
3
+ import { requestJson } from "../../literature/sources/http.js";
4
+ const HandleResponseSchema = z.object({ responseCode: z.number() }).passthrough();
5
+ const RegistrationAgencySchema = z.array(z.object({ DOI: z.string().optional(), RA: z.string().optional() }).passthrough());
6
+ const CslMetadataSchema = z
7
+ .object({
8
+ DOI: z.string().optional(),
9
+ id: z.string().optional(),
10
+ title: z.union([z.string(), z.array(z.string())]).optional(),
11
+ author: z.array(z.object({ given: z.string().optional(), family: z.string().optional(), literal: z.string().optional() }).passthrough()).optional(),
12
+ issued: z.object({ "date-parts": z.array(z.array(z.number())).optional() }).optional(),
13
+ published: z.object({ "date-parts": z.array(z.array(z.number())).optional() }).optional(),
14
+ "container-title": z.union([z.string(), z.array(z.string())]).optional(),
15
+ volume: z.string().optional(),
16
+ page: z.string().optional(),
17
+ URL: z.string().optional(),
18
+ })
19
+ .passthrough();
20
+ function scalar(value) {
21
+ return Array.isArray(value) ? firstNonEmpty(value) : firstNonEmpty([value]);
22
+ }
23
+ function cslRecord(doi, registrationAgency, metadata) {
24
+ const authors = metadata?.author
25
+ ?.map((author) => firstNonEmpty([author.literal, [author.given, author.family].filter(Boolean).join(" ")]))
26
+ .filter((author) => author !== undefined);
27
+ const dateParts = metadata?.issued?.["date-parts"]?.[0] ?? metadata?.published?.["date-parts"]?.[0];
28
+ const title = scalar(metadata?.title);
29
+ const venue = scalar(metadata?.["container-title"]);
30
+ return {
31
+ source: "doi_registry",
32
+ sourceRecordId: doi,
33
+ identifiers: { doi },
34
+ ...(title === undefined ? {} : { title }),
35
+ ...(authors === undefined || authors.length === 0 ? {} : { authors }),
36
+ ...(dateParts?.[0] === undefined ? {} : { year: parseYear(dateParts[0]) }),
37
+ ...(venue === undefined ? {} : { venue }),
38
+ ...(metadata?.volume === undefined ? {} : { volume: metadata.volume }),
39
+ ...(metadata?.page === undefined ? {} : { firstPage: metadata.page.split(/[-–]/)[0] }),
40
+ ...(registrationAgency === undefined ? {} : { registrationAgency }),
41
+ ...(metadata?.URL === undefined ? {} : { url: metadata.URL }),
42
+ };
43
+ }
44
+ export function createDoiRegistryClient(options = {}) {
45
+ return {
46
+ source: "doi_registry",
47
+ async resolve(request, signal) {
48
+ const doi = request.normalized.identifiers.doi;
49
+ if (!request.plan.applicable || doi === undefined) {
50
+ return sourceOutcome("doi_registry", request.plan.operation, "not_applicable", 0, [], request.plan.reason);
51
+ }
52
+ let requestCount = 1;
53
+ const handle = await requestJson(`https://doi.org/api/handles/${encodeDoiPath(doi)}`, HandleResponseSchema, options, signal);
54
+ if (handle.status !== "ok") {
55
+ return handle.status === "no_data"
56
+ ? {
57
+ ...sourceOutcome("doi_registry", request.plan.operation, handle.status, requestCount, [], handle.detail),
58
+ identifierEvidence: { type: "doi", identifier: doi, exists: false, metadataAvailable: false },
59
+ }
60
+ : sourceOutcome("doi_registry", request.plan.operation, handle.status, requestCount, [], handle.detail);
61
+ }
62
+ if (handle.value.responseCode !== 1) {
63
+ return {
64
+ ...sourceOutcome("doi_registry", request.plan.operation, "no_data", requestCount),
65
+ identifierEvidence: { type: "doi", identifier: doi, exists: false, metadataAvailable: false },
66
+ };
67
+ }
68
+ requestCount += 1;
69
+ const agencyResult = await requestJson(`https://doi.org/doiRA/${encodeDoiPath(doi)}`, RegistrationAgencySchema, options, signal);
70
+ const registrationAgency = agencyResult.status === "ok" ? agencyResult.value[0]?.RA : undefined;
71
+ requestCount += 1;
72
+ const metadataResult = await requestJson(`https://doi.org/${encodeDoiPath(doi)}`, CslMetadataSchema, options, signal, {
73
+ headers: { Accept: "application/vnd.citationstyles.csl+json" },
74
+ });
75
+ const metadata = metadataResult.status === "ok" ? metadataResult.value : undefined;
76
+ const incomplete = agencyResult.status === "unavailable" || metadataResult.status === "unavailable";
77
+ const detail = [
78
+ agencyResult.status === "unavailable" ? agencyResult.detail : undefined,
79
+ metadataResult.status === "unavailable" ? metadataResult.detail : undefined,
80
+ ]
81
+ .filter((value) => value !== undefined)
82
+ .join("; ");
83
+ return {
84
+ ...sourceOutcome("doi_registry", request.plan.operation, incomplete ? "unavailable" : "ok", requestCount, [cslRecord(doi, registrationAgency, metadata)], detail || undefined),
85
+ identifierEvidence: {
86
+ type: "doi",
87
+ identifier: doi,
88
+ exists: true,
89
+ ...(registrationAgency === undefined ? {} : { registrationAgency }),
90
+ metadataAvailable: metadata !== undefined,
91
+ },
92
+ };
93
+ },
94
+ };
95
+ }
@@ -0,0 +1,6 @@
1
+ import type { CitationSourceClient } from "../types.js";
2
+ import { type PubmedSourceOptions } from "../../literature/sources/pubmed.js";
3
+ export interface PubmedCitationClientOptions extends PubmedSourceOptions {
4
+ readonly maxCandidates?: number;
5
+ }
6
+ export declare function createPubmedCitationClient(options?: PubmedCitationClientOptions): CitationSourceClient;
@@ -0,0 +1,83 @@
1
+ import { createPubmedSource } from "../../literature/sources/pubmed.js";
2
+ import { parseYear, sourceOutcome } from "./common.js";
3
+ function pubmedRecord(article) {
4
+ const year = parseYear(article.year);
5
+ return {
6
+ source: "pubmed",
7
+ sourceRecordId: article.pmid,
8
+ identifiers: { pmid: article.pmid, ...(article.doi ? { doi: article.doi.toLocaleLowerCase("en-US") } : {}) },
9
+ ...(article.title ? { title: article.title } : {}),
10
+ ...(article.authors.length === 0 ? {} : { authors: article.authors }),
11
+ ...(year === undefined ? {} : { year }),
12
+ ...(article.journal ? { venue: article.journal } : {}),
13
+ url: `https://pubmed.ncbi.nlm.nih.gov/${article.pmid}/`,
14
+ };
15
+ }
16
+ export function createPubmedCitationClient(options = {}) {
17
+ const source = createPubmedSource(options);
18
+ async function resolve(request, signal) {
19
+ if (!request.plan.applicable)
20
+ return sourceOutcome("pubmed", request.plan.operation, "not_applicable", 0, [], request.plan.reason);
21
+ let requestCount = 0;
22
+ let pmids;
23
+ if (request.plan.operation === "pubmed_exact") {
24
+ pmids = [request.normalized.identifiers.pmid];
25
+ }
26
+ else if (request.plan.operation === "pubmed_structured") {
27
+ requestCount += 1;
28
+ const match = await source.matchCitation({
29
+ ...(request.input.venue === undefined ? {} : { venue: request.input.venue }),
30
+ ...(request.input.year === undefined ? {} : { year: request.input.year }),
31
+ ...(request.input.volume === undefined ? {} : { volume: request.input.volume }),
32
+ ...(request.input.firstPage === undefined ? {} : { firstPage: request.input.firstPage }),
33
+ ...(request.input.authors?.[0] === undefined ? {} : { firstAuthor: request.input.authors[0] }),
34
+ key: "citation-0",
35
+ }, signal);
36
+ if (match.status !== "ok")
37
+ return sourceOutcome("pubmed", request.plan.operation, match.status, requestCount, [], match.detail);
38
+ pmids = match.value;
39
+ }
40
+ else {
41
+ requestCount += 1;
42
+ const term = request.plan.operation === "pubmed_doi" ? `${request.normalized.identifiers.doi}[AID]` : `${request.input.title ?? ""}[Title]`;
43
+ const search = await source.searchIds(term, options.maxCandidates ?? 5, signal);
44
+ if (search.status !== "ok")
45
+ return sourceOutcome("pubmed", request.plan.operation, search.status, requestCount, [], search.detail);
46
+ pmids = search.value;
47
+ }
48
+ if (pmids.length === 0)
49
+ return sourceOutcome("pubmed", request.plan.operation, "no_data", requestCount);
50
+ requestCount += 1;
51
+ const fetched = await source.fetchArticles(pmids, signal);
52
+ if (fetched.status !== "ok")
53
+ return sourceOutcome("pubmed", request.plan.operation, fetched.status, requestCount, [], fetched.detail);
54
+ const records = fetched.value.map(pubmedRecord);
55
+ return sourceOutcome("pubmed", request.plan.operation, records.length === 0 ? "no_data" : "ok", requestCount, records);
56
+ }
57
+ async function resolveMany(requests, signal) {
58
+ const results = Array.from({ length: requests.length });
59
+ const exact = requests.flatMap((request, index) => request.plan.applicable && request.plan.operation === "pubmed_exact" && request.normalized.identifiers.pmid !== undefined
60
+ ? [{ index, request, pmid: request.normalized.identifiers.pmid }]
61
+ : []);
62
+ for (let offset = 0; offset < exact.length; offset += 20) {
63
+ const chunk = exact.slice(offset, offset + 20);
64
+ const fetched = await source.fetchArticles(chunk.map(({ pmid }) => pmid), signal);
65
+ if (fetched.status !== "ok") {
66
+ for (const entry of chunk)
67
+ results[entry.index] = sourceOutcome("pubmed", entry.request.plan.operation, fetched.status, 1, [], fetched.detail);
68
+ continue;
69
+ }
70
+ const byPmid = new Map(fetched.value.map((article) => [article.pmid, pubmedRecord(article)]));
71
+ for (const entry of chunk) {
72
+ const record = byPmid.get(entry.pmid);
73
+ results[entry.index] = sourceOutcome("pubmed", entry.request.plan.operation, record === undefined ? "no_data" : "ok", 1, record === undefined ? [] : [record]);
74
+ }
75
+ }
76
+ await Promise.all(requests.map(async (request, index) => {
77
+ if (results[index] === undefined)
78
+ results[index] = await resolve(request, signal);
79
+ }));
80
+ return results.map((result) => result);
81
+ }
82
+ return { source: "pubmed", resolve, resolveMany };
83
+ }
@@ -0,0 +1,6 @@
1
+ import type { CitationSourceClient } from "../types.js";
2
+ import { type SemanticScholarSourceOptions } from "../../literature/sources/semantic-scholar.js";
3
+ export interface SemanticScholarCitationClientOptions extends SemanticScholarSourceOptions {
4
+ readonly maxCandidates?: number;
5
+ }
6
+ export declare function createSemanticScholarCitationClient(options?: SemanticScholarCitationClientOptions): CitationSourceClient;
@@ -0,0 +1,60 @@
1
+ import { createSemanticScholarSource } from "../../literature/sources/semantic-scholar.js";
2
+ import { firstNonEmpty, sourceOutcome } from "./common.js";
3
+ function externalId(value, key) {
4
+ const found = value?.[key];
5
+ return found?.trim() ? found.trim() : undefined;
6
+ }
7
+ function semanticRecord(paper) {
8
+ const doi = externalId(paper.externalIds, "DOI")?.toLocaleLowerCase("en-US");
9
+ const pmid = externalId(paper.externalIds, "PubMed");
10
+ const arxiv = externalId(paper.externalIds, "ArXiv")?.toLocaleLowerCase("en-US");
11
+ const corpusId = firstNonEmpty([externalId(paper.externalIds, "CorpusId"), paper.id]);
12
+ return {
13
+ source: "semantic_scholar",
14
+ sourceRecordId: paper.id,
15
+ identifiers: {
16
+ ...(doi === undefined ? {} : { doi }),
17
+ ...(pmid === undefined ? {} : { pmid }),
18
+ ...(arxiv === undefined ? {} : { arxiv }),
19
+ ...(corpusId === undefined ? {} : { corpusId }),
20
+ },
21
+ ...(paper.title === undefined ? {} : { title: paper.title }),
22
+ ...(paper.authors.length === 0 ? {} : { authors: paper.authors }),
23
+ ...(paper.year === undefined ? {} : { year: paper.year }),
24
+ ...(paper.venue === undefined ? {} : { venue: paper.venue }),
25
+ ...(paper.url === undefined ? {} : { url: paper.url }),
26
+ };
27
+ }
28
+ export function createSemanticScholarCitationClient(options = {}) {
29
+ const source = createSemanticScholarSource(options);
30
+ return {
31
+ source: "semantic_scholar",
32
+ async resolve(request, signal) {
33
+ if (!request.plan.applicable)
34
+ return sourceOutcome("semantic_scholar", request.plan.operation, "not_applicable", 0, [], request.plan.reason);
35
+ const identifier = request.normalized.identifiers.doi
36
+ ? `DOI:${request.normalized.identifiers.doi}`
37
+ : request.normalized.identifiers.pmid
38
+ ? `PMID:${request.normalized.identifiers.pmid}`
39
+ : request.normalized.identifiers.arxiv
40
+ ? `ARXIV:${request.normalized.identifiers.arxiv}`
41
+ : undefined;
42
+ const exact = request.plan.operation === "semantic_scholar_identifier" && identifier !== undefined;
43
+ let papers;
44
+ if (exact) {
45
+ const result = await source.lookupIdentifier(identifier, signal);
46
+ if (result.status !== "ok")
47
+ return sourceOutcome("semantic_scholar", request.plan.operation, result.status, 1, [], result.detail);
48
+ papers = [result.value];
49
+ }
50
+ else {
51
+ const result = await source.search(request.input.title ?? request.input.citation, options.maxCandidates ?? 5, signal);
52
+ if (result.status !== "ok")
53
+ return sourceOutcome("semantic_scholar", request.plan.operation, result.status, 1, [], result.detail);
54
+ papers = result.value;
55
+ }
56
+ const records = papers.map(semanticRecord);
57
+ return sourceOutcome("semantic_scholar", request.plan.operation, records.length === 0 ? "no_data" : "ok", 1, records);
58
+ },
59
+ };
60
+ }
@@ -0,0 +1,9 @@
1
+ import type { CitationCandidateCluster, CitationConflict, CitationFieldComparison, CitationInput } from "./types.js";
2
+ /**
3
+ * Stamped onto every field comparison so a consumer can tell which rules
4
+ * produced it. Bump it when the published comparison semantics change — a
5
+ * revision made before these rules ever shipped is not a new version.
6
+ */
7
+ export declare const CITATION_COMPARISON_RULE_VERSION = "citation-compare-v1";
8
+ export declare function compareSuppliedMetadata(input: CitationInput, selected: CitationCandidateCluster | undefined): CitationFieldComparison[];
9
+ export declare function detectCitationConflicts(clusters: readonly CitationCandidateCluster[]): CitationConflict[];
@@ -0,0 +1,72 @@
1
+ import { authorNamesMatch, normalizeAuthor, normalizeComparable } from "./normalize.js";
2
+ /**
3
+ * Stamped onto every field comparison so a consumer can tell which rules
4
+ * produced it. Bump it when the published comparison semantics change — a
5
+ * revision made before these rules ever shipped is not a new version.
6
+ */
7
+ export const CITATION_COMPARISON_RULE_VERSION = "citation-compare-v1";
8
+ const FIELDS = ["title", "authors", "year", "venue", "volume", "firstPage"];
9
+ function recordValue(record, field) {
10
+ return record[field];
11
+ }
12
+ function normalizedValue(field, value) {
13
+ if (field === "authors")
14
+ return value.map(normalizeAuthor).sort().join("|");
15
+ if (field === "year")
16
+ return String(value);
17
+ return normalizeComparable(String(value));
18
+ }
19
+ function authorMatch(supplied, source) {
20
+ return supplied.every((author) => source.some((candidate) => authorNamesMatch(author, candidate)));
21
+ }
22
+ function fieldMatches(field, supplied, source) {
23
+ if (field === "authors")
24
+ return authorMatch(supplied, source);
25
+ if (field === "year")
26
+ return Math.abs(Number(supplied) - Number(source)) <= 1;
27
+ if (field === "title" || field === "venue") {
28
+ const left = new Set(normalizeComparable(String(supplied)).split(" ").filter(Boolean));
29
+ const right = new Set(normalizeComparable(String(source)).split(" ").filter(Boolean));
30
+ if (left.size === 0 || right.size === 0)
31
+ return false;
32
+ let shared = 0;
33
+ for (const token of left)
34
+ if (right.has(token))
35
+ shared += 1;
36
+ return (2 * shared) / (left.size + right.size) >= 0.92;
37
+ }
38
+ return normalizedValue(field, supplied) === normalizedValue(field, source);
39
+ }
40
+ function sourceValues(cluster, field) {
41
+ return cluster.records.flatMap((record) => {
42
+ const value = recordValue(record, field);
43
+ return value === undefined ? [] : [{ source: record.source, value }];
44
+ });
45
+ }
46
+ export function compareSuppliedMetadata(input, selected) {
47
+ if (selected === undefined)
48
+ return [];
49
+ return FIELDS.flatMap((field) => {
50
+ const supplied = input[field];
51
+ if (supplied === undefined)
52
+ return [];
53
+ const values = sourceValues(selected, field);
54
+ const status = values.length === 0 ? "not_compared" : values.every(({ value }) => fieldMatches(field, supplied, value)) ? "match" : "mismatch";
55
+ return [
56
+ {
57
+ field,
58
+ status,
59
+ supplied,
60
+ sourceValues: values,
61
+ ruleVersion: CITATION_COMPARISON_RULE_VERSION,
62
+ },
63
+ ];
64
+ });
65
+ }
66
+ export function detectCitationConflicts(clusters) {
67
+ return clusters.flatMap((cluster) => FIELDS.flatMap((field) => {
68
+ const values = sourceValues(cluster, field);
69
+ const unique = new Set(values.map(({ value }) => normalizedValue(field, value)));
70
+ return unique.size > 1 ? [{ field, values }] : [];
71
+ }));
72
+ }
@@ -0,0 +1,11 @@
1
+ import type { CitationCandidateCluster, CitationInput, CitationRecord, NormalizedCitation } from "./types.js";
2
+ export interface CitationMatchConfig {
3
+ readonly minimumScore: number;
4
+ readonly separationMargin: number;
5
+ readonly clusterSimilarity: number;
6
+ readonly relatedVersionSimilarity: number;
7
+ }
8
+ export declare const DEFAULT_MATCH_CONFIG: CitationMatchConfig;
9
+ export declare function tokenSimilarity(left: string, right: string): number;
10
+ export declare function clusterCitationRecords(input: CitationInput, normalized: NormalizedCitation, records: readonly CitationRecord[], config?: CitationMatchConfig): CitationCandidateCluster[];
11
+ export declare function selectCitationCluster(clusters: readonly CitationCandidateCluster[], config?: CitationMatchConfig): CitationCandidateCluster | undefined;
@@ -0,0 +1,173 @@
1
+ import { createHash } from "node:crypto";
2
+ import { authorNamesMatch, normalizeAuthor, normalizeComparable } from "./normalize.js";
3
+ export const DEFAULT_MATCH_CONFIG = {
4
+ minimumScore: 0.72,
5
+ separationMargin: 0.08,
6
+ clusterSimilarity: 0.9,
7
+ relatedVersionSimilarity: 0.9,
8
+ };
9
+ function tokens(value) {
10
+ return new Set(normalizeComparable(value)
11
+ .split(" ")
12
+ .filter((token) => token.length > 1));
13
+ }
14
+ export function tokenSimilarity(left, right) {
15
+ const a = tokens(left);
16
+ const b = tokens(right);
17
+ if (a.size === 0 || b.size === 0)
18
+ return 0;
19
+ let intersection = 0;
20
+ for (const token of a)
21
+ if (b.has(token))
22
+ intersection += 1;
23
+ const dice = (2 * intersection) / (a.size + b.size);
24
+ const containment = intersection / Math.min(a.size, b.size);
25
+ return Math.max(dice, containment * 0.96);
26
+ }
27
+ function normalizedAuthors(authors) {
28
+ return [...new Set((authors ?? []).map(normalizeAuthor).filter(Boolean))];
29
+ }
30
+ function authorSimilarity(left, right) {
31
+ const a = normalizedAuthors(left);
32
+ const b = normalizedAuthors(right);
33
+ if (a.length === 0 || b.length === 0)
34
+ return 0;
35
+ const intersection = a.filter((author) => b.some((candidate) => authorNamesMatch(author, candidate))).length;
36
+ return intersection / Math.min(a.length, b.length);
37
+ }
38
+ function identifierEntries(identifiers) {
39
+ return Object.entries(identifiers).flatMap(([kind, value]) => (value === undefined ? [] : [`${kind}:${value.toLocaleLowerCase("en-US")}`]));
40
+ }
41
+ function sharesStrongIdentifier(left, right) {
42
+ const rightIds = new Set(identifierEntries(right.identifiers));
43
+ return identifierEntries(left.identifiers).some((identifier) => rightIds.has(identifier));
44
+ }
45
+ function hasStrongIdentifier(record) {
46
+ return identifierEntries(record.identifiers).length > 0;
47
+ }
48
+ function recordSimilarity(left, right) {
49
+ const weighted = [];
50
+ if (left.title && right.title)
51
+ weighted.push([tokenSimilarity(left.title, right.title), 0.7]);
52
+ if (left.authors && right.authors)
53
+ weighted.push([authorSimilarity(left.authors, right.authors), 0.2]);
54
+ if (left.year !== undefined && right.year !== undefined)
55
+ weighted.push([left.year === right.year ? 1 : Math.abs(left.year - right.year) === 1 ? 0.5 : 0, 0.1]);
56
+ const weight = weighted.reduce((sum, [, value]) => sum + value, 0);
57
+ return weight === 0 ? 0 : weighted.reduce((sum, [score, value]) => sum + score * value, 0) / weight;
58
+ }
59
+ function stableClusterId(records) {
60
+ const identity = records
61
+ .map((record) => `${record.source}:${record.sourceRecordId}`)
62
+ .sort()
63
+ .join("|");
64
+ return `cit-${createHash("sha256").update(identity).digest("hex").slice(0, 16)}`;
65
+ }
66
+ class UnionFind {
67
+ parent;
68
+ constructor(size) {
69
+ this.parent = Array.from({ length: size }, (_, index) => index);
70
+ }
71
+ find(index) {
72
+ const parent = this.parent[index];
73
+ if (parent === index)
74
+ return index;
75
+ const root = this.find(parent);
76
+ this.parent[index] = root;
77
+ return root;
78
+ }
79
+ union(left, right) {
80
+ const leftRoot = this.find(left);
81
+ const rightRoot = this.find(right);
82
+ if (leftRoot !== rightRoot)
83
+ this.parent[rightRoot] = leftRoot;
84
+ }
85
+ }
86
+ function clusterScore(input, normalized, records) {
87
+ const inputIds = new Set(identifierEntries(normalized.identifiers));
88
+ if (records.some((record) => identifierEntries(record.identifiers).some((identifier) => inputIds.has(identifier))))
89
+ return 1;
90
+ let best = 0;
91
+ for (const record of records) {
92
+ const signals = [];
93
+ if (record.title)
94
+ signals.push([tokenSimilarity(normalized.supplied.title ?? normalized.query, record.title), 0.65]);
95
+ if (normalized.supplied.authors && record.authors)
96
+ signals.push([authorSimilarity(normalized.supplied.authors, record.authors), 0.2]);
97
+ if (normalized.supplied.year !== undefined && record.year !== undefined) {
98
+ signals.push([normalized.supplied.year === record.year ? 1 : Math.abs(normalized.supplied.year - record.year) === 1 ? 0.5 : 0, 0.1]);
99
+ }
100
+ if (normalized.supplied.venue && record.venue)
101
+ signals.push([tokenSimilarity(normalized.supplied.venue, record.venue), 0.05]);
102
+ const weight = signals.reduce((sum, [, value]) => sum + value, 0);
103
+ const score = weight === 0 ? 0 : signals.reduce((sum, [value, signalWeight]) => sum + value * signalWeight, 0) / weight;
104
+ best = Math.max(best, score);
105
+ }
106
+ return best;
107
+ }
108
+ function containsId(cluster, key) {
109
+ return cluster.records.some((record) => record.identifiers[key] !== undefined);
110
+ }
111
+ function addRelatedVersions(clusters, config) {
112
+ for (let left = 0; left < clusters.length; left += 1) {
113
+ for (let right = left + 1; right < clusters.length; right += 1) {
114
+ const a = clusters[left];
115
+ const b = clusters[right];
116
+ const similarity = Math.max(...a.records.flatMap((one) => b.records.map((two) => recordSimilarity(one, two))));
117
+ if (similarity < config.relatedVersionSimilarity)
118
+ continue;
119
+ const aArxiv = containsId(a, "arxiv");
120
+ const bArxiv = containsId(b, "arxiv");
121
+ const aDoi = containsId(a, "doi");
122
+ const bDoi = containsId(b, "doi");
123
+ if (aArxiv && bDoi) {
124
+ a.relations.push({ clusterId: b.id, kind: "published_as" });
125
+ b.relations.push({ clusterId: a.id, kind: "preprint_of" });
126
+ }
127
+ else if (bArxiv && aDoi) {
128
+ a.relations.push({ clusterId: b.id, kind: "preprint_of" });
129
+ b.relations.push({ clusterId: a.id, kind: "published_as" });
130
+ }
131
+ else {
132
+ a.relations.push({ clusterId: b.id, kind: "related_version" });
133
+ b.relations.push({ clusterId: a.id, kind: "related_version" });
134
+ }
135
+ }
136
+ }
137
+ }
138
+ export function clusterCitationRecords(input, normalized, records, config = DEFAULT_MATCH_CONFIG) {
139
+ const union = new UnionFind(records.length);
140
+ for (let left = 0; left < records.length; left += 1) {
141
+ for (let right = left + 1; right < records.length; right += 1) {
142
+ const a = records[left];
143
+ const b = records[right];
144
+ if (sharesStrongIdentifier(a, b))
145
+ union.union(left, right);
146
+ else if ((!hasStrongIdentifier(a) || !hasStrongIdentifier(b)) && recordSimilarity(a, b) >= config.clusterSimilarity)
147
+ union.union(left, right);
148
+ }
149
+ }
150
+ const grouped = new Map();
151
+ for (let index = 0; index < records.length; index += 1) {
152
+ const root = union.find(index);
153
+ grouped.set(root, [...(grouped.get(root) ?? []), records[index]]);
154
+ }
155
+ const clusters = [...grouped.values()].map((members) => ({
156
+ id: stableClusterId(members),
157
+ score: clusterScore(input, normalized, members),
158
+ records: members,
159
+ relations: [],
160
+ }));
161
+ clusters.sort((left, right) => right.score - left.score || left.id.localeCompare(right.id));
162
+ addRelatedVersions(clusters, config);
163
+ return clusters;
164
+ }
165
+ export function selectCitationCluster(clusters, config = DEFAULT_MATCH_CONFIG) {
166
+ const first = clusters[0];
167
+ if (first === undefined || first.score < config.minimumScore)
168
+ return undefined;
169
+ const second = clusters[1];
170
+ if (second !== undefined && first.score - second.score < config.separationMargin)
171
+ return undefined;
172
+ return first;
173
+ }
@@ -0,0 +1,31 @@
1
+ import { type CitationInput, type NormalizedCitation } from "./types.js";
2
+ export declare function normalizeComparable(value: string): string;
3
+ export declare function normalizeAuthor(value: string): string;
4
+ /**
5
+ * Whether two author strings can name the same person. Written surname-position
6
+ * free, because citation styles disagree about it (`Smith, Jane A.`,
7
+ * `Jane A. Smith`, `Smith JA` all reach us) and normalization has already
8
+ * discarded the comma that would settle it.
9
+ *
10
+ * Sharing a name part is necessary but never sufficient: two spelled-out names
11
+ * that each carry a part the other lacks are different people, and where both
12
+ * sides supply given-name evidence it has to agree. Surname-only agreement is
13
+ * accepted only when one side supplies no given name at all — the case where
14
+ * there is nothing to contradict.
15
+ */
16
+ export declare function authorNamesMatch(left: string, right: string): boolean;
17
+ export declare function extractDoi(value: string): string | undefined;
18
+ export declare function extractPmid(value: string): string | undefined;
19
+ export declare function extractArxivId(value: string): string | undefined;
20
+ export declare function normalizeCitation(unparsed: CitationInput): NormalizedCitation;
21
+ /**
22
+ * Identity of the *lookups* an input provokes, not of the input itself.
23
+ *
24
+ * Two citations share this key when the authorities would be asked the same
25
+ * question, so they share one round of source outcomes; each caller's own
26
+ * supplied metadata is compared against those outcomes separately. An exact
27
+ * identifier is the whole question — every operation an identifier plan selects
28
+ * is driven by the identifier alone — so supplied metadata stays out of the key
29
+ * there. A free-text citation is dispatched from its metadata, and folds it in.
30
+ */
31
+ export declare function citationLookupKey(input: CitationInput): string;