@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.
- package/dist/agents/conversation-agent.d.ts +3 -0
- package/dist/agents/conversation-agent.js +5 -2
- package/dist/agents/sandbox/shared.d.ts +3 -0
- package/dist/agents/sandbox/shared.js +5 -0
- package/dist/agents/sandbox/types.d.ts +1 -1
- package/dist/app/chat-turn.d.ts +2 -0
- package/dist/app/chat-turn.js +11 -2
- package/dist/app/synthesize-run.d.ts +3 -0
- package/dist/app/synthesize-run.js +2 -1
- package/dist/citations/__fixtures__/resolver.d.ts +3 -0
- package/dist/citations/__fixtures__/resolver.js +9 -0
- package/dist/citations/aggregate.d.ts +3 -0
- package/dist/citations/aggregate.js +44 -0
- package/dist/citations/clients/arxiv.d.ts +6 -0
- package/dist/citations/clients/arxiv.js +34 -0
- package/dist/citations/clients/common.d.ts +11 -0
- package/dist/citations/clients/common.js +43 -0
- package/dist/citations/clients/crossref.d.ts +8 -0
- package/dist/citations/clients/crossref.js +98 -0
- package/dist/citations/clients/doi-registry.d.ts +3 -0
- package/dist/citations/clients/doi-registry.js +95 -0
- package/dist/citations/clients/pubmed.d.ts +6 -0
- package/dist/citations/clients/pubmed.js +83 -0
- package/dist/citations/clients/semantic-scholar.d.ts +6 -0
- package/dist/citations/clients/semantic-scholar.js +60 -0
- package/dist/citations/compare.d.ts +9 -0
- package/dist/citations/compare.js +72 -0
- package/dist/citations/match.d.ts +11 -0
- package/dist/citations/match.js +173 -0
- package/dist/citations/normalize.d.ts +31 -0
- package/dist/citations/normalize.js +187 -0
- package/dist/citations/plan.d.ts +2 -0
- package/dist/citations/plan.js +59 -0
- package/dist/citations/resolve.d.ts +28 -0
- package/dist/citations/resolve.js +238 -0
- package/dist/citations/types.d.ts +583 -0
- package/dist/citations/types.js +129 -0
- package/dist/contracts/chat-events.d.ts +16 -0
- package/dist/contracts/message.d.ts +13 -8
- package/dist/contracts/schemas/chat-events.d.ts +7 -0
- package/dist/contracts/schemas/chat-events.js +2 -0
- package/dist/contracts/schemas/chat-parts.d.ts +8 -8
- package/dist/contracts/target-dossier.d.ts +20 -20
- package/dist/execution/run-synthesis.d.ts +3 -0
- package/dist/execution/run-synthesis.js +1 -0
- package/dist/index.d.ts +14 -10
- package/dist/index.js +20 -16
- package/dist/lib/cache.d.ts +8 -0
- package/dist/lib/cache.js +35 -0
- package/dist/lib/rate-limit.d.ts +35 -0
- package/dist/lib/rate-limit.js +121 -0
- package/dist/literature/sources/arxiv.d.ts +22 -0
- package/dist/literature/sources/arxiv.js +103 -0
- package/dist/literature/sources/http.d.ts +28 -0
- package/dist/literature/sources/http.js +97 -0
- package/dist/literature/sources/pubmed.d.ts +68 -0
- package/dist/literature/sources/pubmed.js +266 -0
- package/dist/literature/sources/semantic-scholar.d.ts +24 -0
- package/dist/literature/sources/semantic-scholar.js +71 -0
- package/dist/memory/content-to-cortex.d.ts +12 -11
- package/dist/memory/content-to-cortex.js +17 -14
- package/dist/memory/conversation-display-backfill.d.ts +25 -0
- package/dist/memory/conversation-display-backfill.js +97 -0
- package/dist/memory/conversation-display-recorder.d.ts +17 -0
- package/dist/memory/conversation-display-recorder.js +156 -0
- package/dist/memory/conversation-display-replay.d.ts +19 -0
- package/dist/memory/conversation-display-replay.js +39 -0
- package/dist/memory/conversation-display-storage.d.ts +86 -0
- package/dist/memory/conversation-display-storage.js +168 -0
- package/dist/memory/reconstruct-cards.d.ts +7 -1
- package/dist/memory/reconstruct-cards.js +5 -1
- package/dist/memory/thread-history.d.ts +46 -11
- package/dist/memory/thread-history.js +55 -13
- package/dist/memory/thread-store.d.ts +142 -25
- package/dist/memory/thread-store.js +228 -40
- package/dist/prompts/literature-reviewer.d.ts +1 -1
- package/dist/prompts/literature-reviewer.js +2 -0
- package/dist/runtime/assemble.d.ts +50 -4
- package/dist/runtime/assemble.js +47 -3
- package/dist/runtime/boot.d.ts +8 -2
- package/dist/runtime/boot.js +20 -2
- package/dist/state/init.js +50 -1
- package/dist/state/schema.d.ts +2 -2
- package/dist/tools/bio/gene-preclinical-profile.d.ts +1 -1
- package/dist/tools/bio/keys.d.ts +1 -0
- package/dist/tools/bio/pubmed.js +4 -4
- package/dist/tools/lib/ncbi-utils.d.ts +3 -58
- package/dist/tools/lib/ncbi-utils.js +2 -286
- package/dist/tools/lib/pubmed-client.d.ts +5 -12
- package/dist/tools/lib/pubmed-client.js +20 -79
- package/dist/tools/research/generate-analogy-report.js +2 -2
- package/dist/tools/research/index.d.ts +1 -0
- package/dist/tools/research/index.js +1 -0
- package/dist/tools/research/inspect-run.d.ts +1 -1
- package/dist/tools/research/literature-reviewer.d.ts +3 -0
- package/dist/tools/research/literature-reviewer.js +2 -0
- package/dist/tools/research/resolve-citation.d.ts +3 -0
- package/dist/tools/research/resolve-citation.js +15 -0
- package/dist/tools/research/search-arxiv.d.ts +3 -19
- package/dist/tools/research/search-arxiv.js +9 -105
- package/dist/tools/research/search-semantic-scholar.d.ts +8 -33
- package/dist/tools/research/search-semantic-scholar.js +29 -88
- package/dist/tools/sandbox/list-available-refs.js +1 -2
- package/dist/workflows/execute-analysis.d.ts +3 -0
- package/dist/workflows/execute-analysis.js +1 -0
- package/dist/workflows/sandbox-step.d.ts +5 -0
- package/dist/workflows/sandbox-step.js +1 -0
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +2 -2
- package/dist/workflows/target-assessment/schemas.d.ts +2 -2
- package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +1 -1
- package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -1
- package/package.json +1 -1
|
@@ -41,6 +41,7 @@ import type { RunLauncher } from "../execution/run-launcher.js";
|
|
|
41
41
|
import { type SubmitReportDeps } from "../tools/iterate-report.js";
|
|
42
42
|
import type { Logger } from "../lib/logger.js";
|
|
43
43
|
import type { UsageRecorder } from "../billing/usage-recorder.js";
|
|
44
|
+
import type { CitationResolver } from "../citations/types.js";
|
|
44
45
|
/** Canonical agent id — the single source of truth. */
|
|
45
46
|
export declare const CONVERSATION_AGENT_ID: "conversation-agent";
|
|
46
47
|
/**
|
|
@@ -58,6 +59,8 @@ export interface ConversationAgentDeps extends EnvironmentStorePaths {
|
|
|
58
59
|
* Omitted falls back to the no-op recorder.
|
|
59
60
|
*/
|
|
60
61
|
readonly usageRecorder?: UsageRecorder;
|
|
62
|
+
/** Shared host-side bibliographic verification service. */
|
|
63
|
+
readonly citationResolver: CitationResolver;
|
|
61
64
|
/** The LLM seam every loop-driving tool runs its sub-agent on. */
|
|
62
65
|
readonly provider: ChatProvider;
|
|
63
66
|
/** Postgres pool — plan persistence, run inspection, workspace index, working memory. */
|
|
@@ -40,13 +40,14 @@ import { createListAvailablePackagesTool } from "../tools/sandbox/list-available
|
|
|
40
40
|
import { createListAvailableRefsTool } from "../tools/sandbox/list-available-refs.js";
|
|
41
41
|
import { createExecuteAnalysisTool } from "../tools/execute-analysis.js";
|
|
42
42
|
import { planReportTool, createReportSubmitTool } from "../tools/iterate-report.js";
|
|
43
|
+
import { createResolveCitationTool } from "../tools/research/resolve-citation.js";
|
|
43
44
|
/** Canonical agent id — the single source of truth. */
|
|
44
45
|
export const CONVERSATION_AGENT_ID = "conversation-agent";
|
|
45
46
|
/** Runaway guard — heavy tool-driving turns need generous headroom. */
|
|
46
47
|
const CONVERSATION_MAX_ITERATIONS = 50;
|
|
47
48
|
/** Build the conversation `AgentDefinition` with every tool fully dep-bound. */
|
|
48
49
|
export function createConversationAgent(deps) {
|
|
49
|
-
const { provider, pool, embedding, workspaceFs, model, utilityProvider, utilityModel, executeAnalysisWorkflow, resolveWorkspaceRoot, runAuthorizer, runLauncher, createPreviewPublisher, bioKeys, templatesDir, skillsDir, chrome, resourcePolicy, hostTools, refStorePath, packagesFile, usageRecorder, } = deps;
|
|
50
|
+
const { provider, pool, embedding, workspaceFs, model, utilityProvider, utilityModel, executeAnalysisWorkflow, resolveWorkspaceRoot, runAuthorizer, runLauncher, createPreviewPublisher, bioKeys, templatesDir, skillsDir, chrome, resourcePolicy, hostTools, refStorePath, packagesFile, usageRecorder, citationResolver, } = deps;
|
|
50
51
|
const workingMemory = createWorkingMemory(pool);
|
|
51
52
|
const ncbi = createNcbiTools(bioKeys);
|
|
52
53
|
const chemDb = createChemDbTools(bioKeys, { ...(deps.logger ? { logger: deps.logger } : {}) });
|
|
@@ -59,6 +60,8 @@ export function createConversationAgent(deps) {
|
|
|
59
60
|
searchInteractionsTool,
|
|
60
61
|
// Literature (search / details / fulltext behind one action).
|
|
61
62
|
ncbi.pubmed,
|
|
63
|
+
// Verification of one caller-supplied citation (distinct from discovery).
|
|
64
|
+
createResolveCitationTool(citationResolver),
|
|
62
65
|
// ChEMBL (compounds / drug / mechanism / bioactivity / targets behind one action).
|
|
63
66
|
chemblTool,
|
|
64
67
|
// PubChem (compound / crossrefs / assays behind one action).
|
|
@@ -132,7 +135,7 @@ export function createConversationAgent(deps) {
|
|
|
132
135
|
createShowPlanTool(pool),
|
|
133
136
|
showFileTool,
|
|
134
137
|
// Batch literature/biology research (sub-agent as a loop-driving tool).
|
|
135
|
-
createLiteratureReviewerTool({ provider, model, bioKeys, usageRecorder }),
|
|
138
|
+
createLiteratureReviewerTool({ provider, model, bioKeys, citationResolver, usageRecorder }),
|
|
136
139
|
// Cross-domain analogy generation (sub-agent as a loop-driving tool).
|
|
137
140
|
createGenerateAnalogyReportTool({ provider, model, bioKeys, usageRecorder }),
|
|
138
141
|
// Workspace semantic search + raw read/grep over the read seam.
|
|
@@ -27,6 +27,7 @@ import { type BlockerHolder } from "../../tools/sandbox/report-blocker.js";
|
|
|
27
27
|
import type { AgentMeta, SandboxToolName } from "./types.js";
|
|
28
28
|
import type { EnvironmentStorePaths } from "../../config/environment-stores.js";
|
|
29
29
|
import type { Logger } from "../../lib/logger.js";
|
|
30
|
+
import type { CitationResolver } from "../../citations/types.js";
|
|
30
31
|
/**
|
|
31
32
|
* Tools every sandbox agent receives — sandbox-environment introspection,
|
|
32
33
|
* library docs, and run inspection. Spread into each agent's
|
|
@@ -74,6 +75,8 @@ export interface SandboxAgentDeps extends EnvironmentStorePaths {
|
|
|
74
75
|
readonly step: SandboxStepCoords;
|
|
75
76
|
/** API keys for the external bio/chem data sources. */
|
|
76
77
|
readonly bioKeys: BioToolKeys;
|
|
78
|
+
/** Shared resolver. Required only when `meta.tools` declares `resolve_citation`. */
|
|
79
|
+
readonly citationResolver?: CitationResolver;
|
|
77
80
|
/**
|
|
78
81
|
* Per-run blocker cell (see the harness-sandbox-agents spec). When present, the agent gets a
|
|
79
82
|
* `report_blocker` tool that records `{ kind: "blocker", reason }` into it;
|
|
@@ -38,6 +38,7 @@ import { createReportBlockerTool } from "../../tools/sandbox/report-blocker.js";
|
|
|
38
38
|
import { toSandboxPath } from "../../workspace/paths.js";
|
|
39
39
|
import { setActiveExecId } from "../../state/index.js";
|
|
40
40
|
import { SANDBOX_AGENT_DEFAULT_MAX_ITERATIONS } from "./types.js";
|
|
41
|
+
import { createResolveCitationTool } from "../../tools/research/resolve-citation.js";
|
|
41
42
|
/**
|
|
42
43
|
* Tools every sandbox agent receives — sandbox-environment introspection,
|
|
43
44
|
* library docs, and run inspection. Spread into each agent's
|
|
@@ -53,6 +54,9 @@ export const BASE_SANDBOX_TOOLS = ["listAvailablePackages", "listAvailableRefs",
|
|
|
53
54
|
function resolveSandboxTools(deps, tools) {
|
|
54
55
|
const ncbi = createNcbiTools(deps.bioKeys);
|
|
55
56
|
const chemDb = createChemDbTools(deps.bioKeys, { ...(deps.logger ? { logger: deps.logger } : {}) });
|
|
57
|
+
if (tools.includes("resolve_citation") && deps.citationResolver === undefined) {
|
|
58
|
+
throw new Error('createSandboxAgent: SandboxToolName "resolve_citation" requires a CitationResolver dependency.');
|
|
59
|
+
}
|
|
56
60
|
const registry = {
|
|
57
61
|
listAvailablePackages: createListAvailablePackagesTool({ ...(deps.packagesFile ? { packagesFile: deps.packagesFile } : {}) }),
|
|
58
62
|
listAvailableRefs: createListAvailableRefsTool({ ...(deps.refStorePath ? { refStorePath: deps.refStorePath } : {}) }),
|
|
@@ -60,6 +64,7 @@ function resolveSandboxTools(deps, tools) {
|
|
|
60
64
|
queryDocs: queryDocsTool,
|
|
61
65
|
inspectRun: createInspectRunTool(deps.pool),
|
|
62
66
|
pubmed: ncbi.pubmed,
|
|
67
|
+
resolve_citation: deps.citationResolver === undefined ? undefined : createResolveCitationTool(deps.citationResolver),
|
|
63
68
|
searchGene: searchGeneTool,
|
|
64
69
|
lookupAnnotation: lookupAnnotationTool,
|
|
65
70
|
searchInteractions: searchInteractionsTool,
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* in the tool's `perSource` report; the rest of the call still answers.
|
|
13
13
|
*/
|
|
14
14
|
/** Closed allowlist of tools any sandbox agent may declare. */
|
|
15
|
-
export type SandboxToolName = "listAvailablePackages" | "listAvailableRefs" | "resolveLibraryId" | "queryDocs" | "inspectRun" | "pubmed" | "searchGene" | "lookupAnnotation" | "searchInteractions" | "chembl" | "pubchem" | "opentargets" | "geneDiseaseEvidence" | "drugGeneInteractions" | "genePreclinicalProfile" | "searchFaers" | "searchClinicalTrials" | "searchGeoDatasets" | "targetSafety" | "comptox";
|
|
15
|
+
export type SandboxToolName = "listAvailablePackages" | "listAvailableRefs" | "resolveLibraryId" | "queryDocs" | "inspectRun" | "pubmed" | "resolve_citation" | "searchGene" | "lookupAnnotation" | "searchInteractions" | "chembl" | "pubchem" | "opentargets" | "geneDiseaseEvidence" | "drugGeneInteractions" | "genePreclinicalProfile" | "searchFaers" | "searchClinicalTrials" | "searchGeoDatasets" | "targetSafety" | "comptox";
|
|
16
16
|
/** Planner-facing metadata + tool allowlist for one sandbox agent. */
|
|
17
17
|
export interface AgentMeta {
|
|
18
18
|
/** Stable agent id — also the key in the catalog and the `AgentDefinition.id`. */
|
package/dist/app/chat-turn.d.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* assembled `messages` and the standalone `userMessage` the caller persists.
|
|
14
14
|
*/
|
|
15
15
|
import type { Pool } from "pg";
|
|
16
|
+
import { type ThreadType } from "../memory/thread-store.js";
|
|
16
17
|
import { type AssembledMessages } from "./message-assembly.js";
|
|
17
18
|
import type { Logger } from "../lib/logger.js";
|
|
18
19
|
export interface PrepareChatTurnDeps {
|
|
@@ -27,6 +28,7 @@ export interface PrepareChatTurnParams {
|
|
|
27
28
|
}
|
|
28
29
|
export type PrepareChatTurnResult = ({
|
|
29
30
|
readonly kind: "ok";
|
|
31
|
+
readonly threadType: ThreadType;
|
|
30
32
|
} & AssembledMessages) | {
|
|
31
33
|
readonly kind: "not_found";
|
|
32
34
|
};
|
package/dist/app/chat-turn.js
CHANGED
|
@@ -36,14 +36,23 @@ export async function prepareChatTurn(deps, params) {
|
|
|
36
36
|
if (existing && existing.analysisId !== analysisId) {
|
|
37
37
|
return { kind: "not_found" };
|
|
38
38
|
}
|
|
39
|
+
// The type a caller resolves the turn's agent from. An existing row carries
|
|
40
|
+
// its own. An absent one defaults to `conversation` — the store's own
|
|
41
|
+
// default — so a best-effort create that fails non-fatally still leaves a
|
|
42
|
+
// usable type; a successful create overrides it from the returned row.
|
|
43
|
+
let threadType = existing ? existing.threadType : "conversation";
|
|
39
44
|
// Seed the thread title from the first user message. Best-effort.
|
|
40
45
|
try {
|
|
41
46
|
if (!existing) {
|
|
42
|
-
|
|
47
|
+
// `createThread` is idempotent (ON CONFLICT reads the row back), so
|
|
48
|
+
// the returned row is authoritative for the type: a create that
|
|
49
|
+
// races another writer reflects the stored type, not an assumed one.
|
|
50
|
+
const created = unwrapOrThrow(await store.createThread({
|
|
43
51
|
threadId,
|
|
44
52
|
analysisId,
|
|
45
53
|
title: deriveThreadTitle(userInput),
|
|
46
54
|
}));
|
|
55
|
+
threadType = created.threadType;
|
|
47
56
|
}
|
|
48
57
|
else if (!existing.title || existing.title.length === 0) {
|
|
49
58
|
unwrapOrThrow(await store.updateTitle(threadId, deriveThreadTitle(userInput)));
|
|
@@ -65,5 +74,5 @@ export async function prepareChatTurn(deps, params) {
|
|
|
65
74
|
history,
|
|
66
75
|
workingMemory: createWorkingMemory(pool),
|
|
67
76
|
});
|
|
68
|
-
return { kind: "ok", messages, userMessage };
|
|
77
|
+
return { kind: "ok", threadType, messages, userMessage };
|
|
69
78
|
}
|
|
@@ -25,6 +25,7 @@ import type { ResolveWorkspaceRoot } from "../workspace/paths.js";
|
|
|
25
25
|
import type { SynthesisStatus } from "../state/index.js";
|
|
26
26
|
import type { Logger } from "../lib/logger.js";
|
|
27
27
|
import type { UsageRecorder } from "../billing/usage-recorder.js";
|
|
28
|
+
import type { CitationResolver } from "../citations/types.js";
|
|
28
29
|
export interface SynthesizeRunDeps {
|
|
29
30
|
/** Operational logging seam; omitted falls back to no-op. */
|
|
30
31
|
readonly logger?: Logger;
|
|
@@ -38,6 +39,8 @@ export interface SynthesizeRunDeps {
|
|
|
38
39
|
readonly synthesisModel: string;
|
|
39
40
|
/** API keys for the bio/chem tools the embedded literature reviewer uses. */
|
|
40
41
|
readonly bioKeys: BioToolKeys;
|
|
42
|
+
/** Shared resolver used by the embedded literature reviewer. */
|
|
43
|
+
readonly citationResolver: CitationResolver;
|
|
41
44
|
/** LLM usage-accounting seam for the synthesizer loop; omitted falls back to the no-op recorder. */
|
|
42
45
|
readonly usageRecorder?: UsageRecorder;
|
|
43
46
|
}
|
|
@@ -22,7 +22,7 @@ import { loadPlan, queryRun } from "../state/index.js";
|
|
|
22
22
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
23
23
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
24
24
|
export async function synthesizeRun(deps, params) {
|
|
25
|
-
const { pool, provider, embedding, resolveWorkspaceRoot, synthesisModel, bioKeys, usageRecorder } = deps;
|
|
25
|
+
const { pool, provider, embedding, resolveWorkspaceRoot, synthesisModel, bioKeys, citationResolver, usageRecorder } = deps;
|
|
26
26
|
const { analysisId, runId, completedSteps, session, emit, onProgress } = params;
|
|
27
27
|
await onProgress("starting", "Beginning literature-grounded synthesis");
|
|
28
28
|
try {
|
|
@@ -47,6 +47,7 @@ export async function synthesizeRun(deps, params) {
|
|
|
47
47
|
session: synthesizerSession,
|
|
48
48
|
model: synthesisModel,
|
|
49
49
|
bioKeys,
|
|
50
|
+
citationResolver,
|
|
50
51
|
usageRecorder,
|
|
51
52
|
summaries,
|
|
52
53
|
planNarrative,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Test-only resolver for construction paths that do not execute citation tools. */
|
|
2
|
+
export const unusedCitationResolver = {
|
|
3
|
+
async resolveOne() {
|
|
4
|
+
throw new Error("unusedCitationResolver.resolveOne was unexpectedly called");
|
|
5
|
+
},
|
|
6
|
+
async resolveMany() {
|
|
7
|
+
throw new Error("unusedCitationResolver.resolveMany was unexpectedly called");
|
|
8
|
+
},
|
|
9
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type CitationMatchConfig } from "./match.js";
|
|
2
|
+
import type { CitationInput, CitationResolutionResult, CitationSourceOutcome, NormalizedCitation } from "./types.js";
|
|
3
|
+
export declare function aggregateCitationResolution(input: CitationInput, normalized: NormalizedCitation, sourceOutcomes: readonly CitationSourceOutcome[], matchConfig?: CitationMatchConfig): CitationResolutionResult;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { compareSuppliedMetadata, detectCitationConflicts } from "./compare.js";
|
|
2
|
+
import { clusterCitationRecords, selectCitationCluster, DEFAULT_MATCH_CONFIG } from "./match.js";
|
|
3
|
+
function deriveCoverage(outcomes, comparisonsIncomplete) {
|
|
4
|
+
const applicable = outcomes.filter((outcome) => outcome.status !== "not_applicable");
|
|
5
|
+
if (applicable.length === 0)
|
|
6
|
+
return "none";
|
|
7
|
+
const hasCoverage = applicable.some((outcome) => outcome.status === "ok" || outcome.status === "no_data" || outcome.records.length > 0 || outcome.identifierEvidence?.exists === true);
|
|
8
|
+
if (!hasCoverage)
|
|
9
|
+
return "none";
|
|
10
|
+
if (applicable.some((outcome) => outcome.status === "unavailable") || comparisonsIncomplete)
|
|
11
|
+
return "partial";
|
|
12
|
+
return "complete";
|
|
13
|
+
}
|
|
14
|
+
function deriveVerdict(normalized, selected, comparisons, coverage) {
|
|
15
|
+
if (normalized.unsupportedWorkKind !== undefined)
|
|
16
|
+
return "unverifiable";
|
|
17
|
+
if (selected !== undefined) {
|
|
18
|
+
if (comparisons.some((comparison) => comparison.status === "mismatch"))
|
|
19
|
+
return "metadata_mismatch";
|
|
20
|
+
if (comparisons.some((comparison) => comparison.status === "not_compared"))
|
|
21
|
+
return "inconclusive";
|
|
22
|
+
return "verified";
|
|
23
|
+
}
|
|
24
|
+
return coverage === "complete" ? "not_found" : "inconclusive";
|
|
25
|
+
}
|
|
26
|
+
export function aggregateCitationResolution(input, normalized, sourceOutcomes, matchConfig = DEFAULT_MATCH_CONFIG) {
|
|
27
|
+
const records = sourceOutcomes.flatMap((outcome) => outcome.records);
|
|
28
|
+
const candidates = clusterCitationRecords(input, normalized, records, matchConfig);
|
|
29
|
+
const selected = selectCitationCluster(candidates, matchConfig);
|
|
30
|
+
const comparisons = compareSuppliedMetadata(input, selected);
|
|
31
|
+
const coverage = deriveCoverage(sourceOutcomes, comparisons.some((comparison) => comparison.status === "not_compared"));
|
|
32
|
+
const verdict = deriveVerdict(normalized, selected, comparisons, coverage);
|
|
33
|
+
return {
|
|
34
|
+
input,
|
|
35
|
+
normalized,
|
|
36
|
+
sourceOutcomes: [...sourceOutcomes],
|
|
37
|
+
candidates,
|
|
38
|
+
...(selected === undefined ? {} : { selectedClusterId: selected.id }),
|
|
39
|
+
comparisons,
|
|
40
|
+
conflicts: detectCitationConflicts(candidates),
|
|
41
|
+
verdict,
|
|
42
|
+
coverage,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CitationSourceClient } from "../types.js";
|
|
2
|
+
import type { SourceHttpOptions } from "../../literature/sources/http.js";
|
|
3
|
+
export interface ArxivCitationClientOptions extends SourceHttpOptions {
|
|
4
|
+
readonly maxCandidates?: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function createArxivCitationClient(options?: ArxivCitationClientOptions): CitationSourceClient;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { extractArxivId } from "../normalize.js";
|
|
2
|
+
import { createArxivSource } from "../../literature/sources/arxiv.js";
|
|
3
|
+
import { parseYear, sourceOutcome } from "./common.js";
|
|
4
|
+
function arxivRecord(paper) {
|
|
5
|
+
const year = parseYear(paper.published);
|
|
6
|
+
const identifier = extractArxivId(paper.id) ?? paper.id.toLocaleLowerCase("en-US");
|
|
7
|
+
return {
|
|
8
|
+
source: "arxiv",
|
|
9
|
+
sourceRecordId: paper.id,
|
|
10
|
+
identifiers: { arxiv: identifier },
|
|
11
|
+
title: paper.title,
|
|
12
|
+
authors: paper.authors,
|
|
13
|
+
...(year === undefined ? {} : { year }),
|
|
14
|
+
venue: "arXiv",
|
|
15
|
+
url: paper.url,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function createArxivCitationClient(options = {}) {
|
|
19
|
+
const source = createArxivSource(options);
|
|
20
|
+
return {
|
|
21
|
+
source: "arxiv",
|
|
22
|
+
async resolve(request, signal) {
|
|
23
|
+
if (!request.plan.applicable)
|
|
24
|
+
return sourceOutcome("arxiv", request.plan.operation, "not_applicable", 0, [], request.plan.reason);
|
|
25
|
+
const result = request.plan.operation === "arxiv_exact"
|
|
26
|
+
? await source.lookupExact(request.normalized.identifiers.arxiv, options.maxCandidates ?? 5, signal)
|
|
27
|
+
: await source.search({ query: request.input.title ?? request.input.citation, limit: options.maxCandidates ?? 5 }, signal);
|
|
28
|
+
if (result.status !== "ok")
|
|
29
|
+
return sourceOutcome("arxiv", request.plan.operation, result.status, 1, [], result.detail);
|
|
30
|
+
const records = result.value.map(arxivRecord);
|
|
31
|
+
return sourceOutcome("arxiv", request.plan.operation, records.length === 0 ? "no_data" : "ok", 1, records);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CitationRecord, CitationSource, CitationSourceOperation, CitationSourceOutcome, CitationSourcePlanItem } from "../types.js";
|
|
2
|
+
export declare function notApplicableOutcome(plan: CitationSourcePlanItem): CitationSourceOutcome;
|
|
3
|
+
export declare function sourceOutcome(source: CitationSource, operation: CitationSourceOperation, status: CitationSourceOutcome["status"], requestCount: number, records?: CitationRecord[], detail?: string): CitationSourceOutcome;
|
|
4
|
+
/**
|
|
5
|
+
* Percent-encode a DOI for use as a URL path segment. A DOI suffix may legally
|
|
6
|
+
* contain `#`, `?`, and `%`, each of which silently re-parses the URL when
|
|
7
|
+
* interpolated raw; only the prefix/suffix `/` is structural and stays literal.
|
|
8
|
+
*/
|
|
9
|
+
export declare function encodeDoiPath(doi: string): string;
|
|
10
|
+
export declare function parseYear(value: unknown): number | undefined;
|
|
11
|
+
export declare function firstNonEmpty(values: readonly unknown[]): string | undefined;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function notApplicableOutcome(plan) {
|
|
2
|
+
return {
|
|
3
|
+
source: plan.source,
|
|
4
|
+
operation: plan.operation,
|
|
5
|
+
status: "not_applicable",
|
|
6
|
+
requestCount: 0,
|
|
7
|
+
records: [],
|
|
8
|
+
detail: plan.reason,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function sourceOutcome(source, operation, status, requestCount, records = [], detail) {
|
|
12
|
+
return {
|
|
13
|
+
source,
|
|
14
|
+
operation,
|
|
15
|
+
status,
|
|
16
|
+
requestCount,
|
|
17
|
+
records,
|
|
18
|
+
...(detail === undefined ? {} : { detail }),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Percent-encode a DOI for use as a URL path segment. A DOI suffix may legally
|
|
23
|
+
* contain `#`, `?`, and `%`, each of which silently re-parses the URL when
|
|
24
|
+
* interpolated raw; only the prefix/suffix `/` is structural and stays literal.
|
|
25
|
+
*/
|
|
26
|
+
export function encodeDoiPath(doi) {
|
|
27
|
+
return doi.split("/").map(encodeURIComponent).join("/");
|
|
28
|
+
}
|
|
29
|
+
export function parseYear(value) {
|
|
30
|
+
if (typeof value === "number" && Number.isInteger(value))
|
|
31
|
+
return value;
|
|
32
|
+
if (typeof value !== "string")
|
|
33
|
+
return undefined;
|
|
34
|
+
const match = value.match(/\b(1[5-9]\d{2}|20\d{2}|21\d{2})\b/);
|
|
35
|
+
return match?.[1] === undefined ? undefined : Number(match[1]);
|
|
36
|
+
}
|
|
37
|
+
export function firstNonEmpty(values) {
|
|
38
|
+
for (const value of values) {
|
|
39
|
+
if (typeof value === "string" && value.trim())
|
|
40
|
+
return value.trim();
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CitationSourceClient } from "../types.js";
|
|
2
|
+
import { type SourceHttpOptions } from "../../literature/sources/http.js";
|
|
3
|
+
export interface CrossrefClientOptions extends SourceHttpOptions {
|
|
4
|
+
readonly mailto?: string;
|
|
5
|
+
readonly userAgent?: string;
|
|
6
|
+
readonly maxCandidates?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function createCrossrefClient(options?: CrossrefClientOptions): CitationSourceClient;
|
|
@@ -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,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;
|