@inflexa-ai/harness 0.15.1 → 0.17.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 (112) 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/sandbox/shared.d.ts +3 -0
  4. package/dist/agents/sandbox/shared.js +5 -0
  5. package/dist/agents/sandbox/types.d.ts +1 -1
  6. package/dist/app/chat-turn.d.ts +2 -0
  7. package/dist/app/chat-turn.js +11 -2
  8. package/dist/app/synthesize-run.d.ts +3 -0
  9. package/dist/app/synthesize-run.js +2 -1
  10. package/dist/citations/__fixtures__/resolver.d.ts +3 -0
  11. package/dist/citations/__fixtures__/resolver.js +9 -0
  12. package/dist/citations/aggregate.d.ts +3 -0
  13. package/dist/citations/aggregate.js +44 -0
  14. package/dist/citations/clients/arxiv.d.ts +6 -0
  15. package/dist/citations/clients/arxiv.js +34 -0
  16. package/dist/citations/clients/common.d.ts +11 -0
  17. package/dist/citations/clients/common.js +43 -0
  18. package/dist/citations/clients/crossref.d.ts +8 -0
  19. package/dist/citations/clients/crossref.js +98 -0
  20. package/dist/citations/clients/doi-registry.d.ts +3 -0
  21. package/dist/citations/clients/doi-registry.js +95 -0
  22. package/dist/citations/clients/pubmed.d.ts +6 -0
  23. package/dist/citations/clients/pubmed.js +83 -0
  24. package/dist/citations/clients/semantic-scholar.d.ts +6 -0
  25. package/dist/citations/clients/semantic-scholar.js +60 -0
  26. package/dist/citations/compare.d.ts +9 -0
  27. package/dist/citations/compare.js +72 -0
  28. package/dist/citations/match.d.ts +11 -0
  29. package/dist/citations/match.js +173 -0
  30. package/dist/citations/normalize.d.ts +31 -0
  31. package/dist/citations/normalize.js +187 -0
  32. package/dist/citations/plan.d.ts +2 -0
  33. package/dist/citations/plan.js +59 -0
  34. package/dist/citations/resolve.d.ts +28 -0
  35. package/dist/citations/resolve.js +238 -0
  36. package/dist/citations/types.d.ts +583 -0
  37. package/dist/citations/types.js +129 -0
  38. package/dist/contracts/chat-events.d.ts +16 -0
  39. package/dist/contracts/message.d.ts +13 -8
  40. package/dist/contracts/schemas/chat-events.d.ts +7 -0
  41. package/dist/contracts/schemas/chat-events.js +2 -0
  42. package/dist/contracts/schemas/chat-parts.d.ts +8 -8
  43. package/dist/contracts/target-dossier.d.ts +20 -20
  44. package/dist/execution/run-synthesis.d.ts +3 -0
  45. package/dist/execution/run-synthesis.js +1 -0
  46. package/dist/index.d.ts +14 -10
  47. package/dist/index.js +20 -16
  48. package/dist/lib/cache.d.ts +8 -0
  49. package/dist/lib/cache.js +35 -0
  50. package/dist/lib/rate-limit.d.ts +35 -0
  51. package/dist/lib/rate-limit.js +121 -0
  52. package/dist/literature/sources/arxiv.d.ts +22 -0
  53. package/dist/literature/sources/arxiv.js +103 -0
  54. package/dist/literature/sources/http.d.ts +28 -0
  55. package/dist/literature/sources/http.js +97 -0
  56. package/dist/literature/sources/pubmed.d.ts +68 -0
  57. package/dist/literature/sources/pubmed.js +266 -0
  58. package/dist/literature/sources/semantic-scholar.d.ts +24 -0
  59. package/dist/literature/sources/semantic-scholar.js +71 -0
  60. package/dist/memory/content-to-cortex.d.ts +12 -11
  61. package/dist/memory/content-to-cortex.js +17 -14
  62. package/dist/memory/conversation-display-backfill.d.ts +25 -0
  63. package/dist/memory/conversation-display-backfill.js +97 -0
  64. package/dist/memory/conversation-display-recorder.d.ts +17 -0
  65. package/dist/memory/conversation-display-recorder.js +156 -0
  66. package/dist/memory/conversation-display-replay.d.ts +19 -0
  67. package/dist/memory/conversation-display-replay.js +39 -0
  68. package/dist/memory/conversation-display-storage.d.ts +86 -0
  69. package/dist/memory/conversation-display-storage.js +168 -0
  70. package/dist/memory/reconstruct-cards.d.ts +7 -1
  71. package/dist/memory/reconstruct-cards.js +5 -1
  72. package/dist/memory/thread-history.d.ts +46 -11
  73. package/dist/memory/thread-history.js +55 -13
  74. package/dist/memory/thread-store.d.ts +142 -25
  75. package/dist/memory/thread-store.js +228 -40
  76. package/dist/prompts/literature-reviewer.d.ts +1 -1
  77. package/dist/prompts/literature-reviewer.js +2 -0
  78. package/dist/runtime/assemble.d.ts +50 -4
  79. package/dist/runtime/assemble.js +47 -3
  80. package/dist/runtime/boot.d.ts +8 -2
  81. package/dist/runtime/boot.js +20 -2
  82. package/dist/state/init.js +50 -1
  83. package/dist/state/schema.d.ts +2 -2
  84. package/dist/tools/bio/gene-preclinical-profile.d.ts +1 -1
  85. package/dist/tools/bio/keys.d.ts +1 -0
  86. package/dist/tools/bio/pubmed.js +4 -4
  87. package/dist/tools/lib/ncbi-utils.d.ts +3 -58
  88. package/dist/tools/lib/ncbi-utils.js +2 -286
  89. package/dist/tools/lib/pubmed-client.d.ts +5 -12
  90. package/dist/tools/lib/pubmed-client.js +20 -79
  91. package/dist/tools/research/generate-analogy-report.js +2 -2
  92. package/dist/tools/research/index.d.ts +1 -0
  93. package/dist/tools/research/index.js +1 -0
  94. package/dist/tools/research/inspect-run.d.ts +1 -1
  95. package/dist/tools/research/literature-reviewer.d.ts +3 -0
  96. package/dist/tools/research/literature-reviewer.js +2 -0
  97. package/dist/tools/research/resolve-citation.d.ts +3 -0
  98. package/dist/tools/research/resolve-citation.js +15 -0
  99. package/dist/tools/research/search-arxiv.d.ts +3 -19
  100. package/dist/tools/research/search-arxiv.js +9 -105
  101. package/dist/tools/research/search-semantic-scholar.d.ts +8 -33
  102. package/dist/tools/research/search-semantic-scholar.js +29 -88
  103. package/dist/tools/sandbox/list-available-refs.js +1 -2
  104. package/dist/workflows/execute-analysis.d.ts +3 -0
  105. package/dist/workflows/execute-analysis.js +1 -0
  106. package/dist/workflows/sandbox-step.d.ts +5 -0
  107. package/dist/workflows/sandbox-step.js +1 -0
  108. package/dist/workflows/target-assessment/phase4-assemble.d.ts +2 -2
  109. package/dist/workflows/target-assessment/schemas.d.ts +2 -2
  110. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +1 -1
  111. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -1
  112. package/package.json +1 -1
@@ -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
+ }