@inflexa-ai/harness 0.17.0 → 0.19.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/report-builder.d.ts +2 -0
- package/dist/agents/report-builder.js +2 -0
- package/dist/agents/report-session-agent.d.ts +56 -0
- package/dist/agents/report-session-agent.js +72 -0
- package/dist/app/report-session-runtime.d.ts +72 -0
- package/dist/app/report-session-runtime.js +157 -0
- package/dist/app/spawn-report-session.d.ts +85 -0
- package/dist/app/spawn-report-session.js +83 -0
- package/dist/contracts/chat-events.d.ts +9 -0
- package/dist/contracts/index.d.ts +6 -2
- package/dist/contracts/index.js +3 -1
- package/dist/contracts/organ-system.d.ts +49 -0
- package/dist/contracts/organ-system.js +67 -0
- package/dist/contracts/report-blocks.d.ts +564 -0
- package/dist/contracts/report-blocks.js +117 -0
- package/dist/contracts/report-reference.d.ts +459 -0
- package/dist/contracts/report-reference.js +256 -0
- package/dist/contracts/schemas/chat-events.d.ts +2 -0
- package/dist/contracts/schemas/chat-events.js +2 -0
- package/dist/contracts/severity.d.ts +15 -0
- package/dist/contracts/severity.js +10 -0
- package/dist/contracts/target-dossier.d.ts +8675 -12972
- package/dist/contracts/target-dossier.js +534 -591
- package/dist/data/safety-panel-schema.d.ts +18 -5
- package/dist/data/safety-panel-schema.js +4 -4
- package/dist/execution/artifact-metadata.js +1 -0
- package/dist/execution/report-runner.js +1 -3
- package/dist/execution/run-synthesis.js +2 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +19 -0
- package/dist/lib/async-utils.d.ts +16 -4
- package/dist/lib/async-utils.js +20 -15
- package/dist/lib/host-concurrency.js +1 -0
- package/dist/loop/__fixtures__/scripted-provider.js +23 -2
- package/dist/loop/run-agent.js +58 -10
- package/dist/loop/tool-detail.d.ts +40 -0
- package/dist/loop/tool-detail.js +35 -3
- package/dist/loop/types.d.ts +13 -0
- package/dist/memory/thread-history.d.ts +10 -0
- package/dist/memory/thread-history.js +12 -1
- package/dist/memory/thread-store.d.ts +4 -4
- package/dist/memory/thread-store.js +18 -9
- package/dist/prompts/report-session.d.ts +1 -0
- package/dist/prompts/report-session.js +89 -0
- package/dist/prompts/target-assessment/briefs/executive-recommendation.d.ts +1 -1
- package/dist/prompts/target-assessment/briefs/executive-recommendation.js +5 -5
- package/dist/prompts/target-assessment/briefs/liability-bullets.d.ts +1 -1
- package/dist/prompts/target-assessment/briefs/liability-bullets.js +1 -1
- package/dist/prompts/target-assessment/briefs/target-organ-liabilities.d.ts +1 -1
- package/dist/prompts/target-assessment/briefs/target-organ-liabilities.js +11 -3
- package/dist/prompts/target-assessment/investigation/adversarial-critique.d.ts +1 -0
- package/dist/prompts/target-assessment/investigation/adversarial-critique.js +67 -0
- package/dist/prompts/target-assessment/investigation/claim-reverification.d.ts +1 -0
- package/dist/prompts/target-assessment/investigation/claim-reverification.js +49 -0
- package/dist/prompts/target-assessment/investigation/index.d.ts +3 -0
- package/dist/prompts/target-assessment/investigation/index.js +3 -0
- package/dist/prompts/target-assessment/investigation/mechanism-proposal.d.ts +1 -0
- package/dist/prompts/target-assessment/investigation/mechanism-proposal.js +49 -0
- package/dist/prompts/target-assessment/tox-voice/index.d.ts +1 -1
- package/dist/prompts/target-assessment/tox-voice/vocabulary.d.ts +8 -5
- package/dist/prompts/target-assessment/tox-voice/vocabulary.js +9 -20
- package/dist/providers/__fixtures__/session.d.ts +5 -4
- package/dist/providers/__fixtures__/session.js +4 -4
- package/dist/providers/ai-sdk.d.ts +77 -0
- package/dist/providers/ai-sdk.js +149 -19
- package/dist/providers/errors.d.ts +14 -0
- package/dist/providers/errors.js +103 -18
- package/dist/providers/types.d.ts +16 -0
- package/dist/providers/types.js +10 -1
- package/dist/report-model/block-walk.d.ts +56 -0
- package/dist/report-model/block-walk.js +89 -0
- package/dist/report-model/draft-finish.d.ts +60 -0
- package/dist/report-model/draft-finish.js +56 -0
- package/dist/report-model/draft-operations.d.ts +125 -0
- package/dist/report-model/draft-operations.js +341 -0
- package/dist/report-model/draft-read.d.ts +59 -0
- package/dist/report-model/draft-read.js +109 -0
- package/dist/report-model/draft.d.ts +51 -0
- package/dist/report-model/draft.js +32 -0
- package/dist/report-model/fixture-resolver.d.ts +8 -0
- package/dist/report-model/fixture-resolver.js +252 -0
- package/dist/report-model/pin-snapshot.d.ts +30 -0
- package/dist/report-model/pin-snapshot.js +40 -0
- package/dist/report-model/reference-resolver.d.ts +113 -0
- package/dist/report-model/reference-resolver.js +60 -0
- package/dist/report-model/structural-validation.d.ts +22 -0
- package/dist/report-model/structural-validation.js +68 -0
- package/dist/report-model/validate.d.ts +72 -0
- package/dist/report-model/validate.js +95 -0
- package/dist/report-render/chart.d.ts +33 -0
- package/dist/report-render/chart.js +538 -0
- package/dist/report-render/page.d.ts +127 -0
- package/dist/report-render/page.js +307 -0
- package/dist/report-render/references.d.ts +32 -0
- package/dist/report-render/references.js +44 -0
- package/dist/report-render/render.d.ts +25 -0
- package/dist/report-render/render.js +125 -0
- package/dist/report-render/script-json.d.ts +13 -0
- package/dist/report-render/script-json.js +15 -0
- package/dist/report-render/types.d.ts +39 -0
- package/dist/report-render/types.js +10 -0
- package/dist/report-render/value-bridge.d.ts +62 -0
- package/dist/report-render/value-bridge.js +82 -0
- package/dist/report-render/views/chart-view.d.ts +11 -0
- package/dist/report-render/views/chart-view.js +23 -0
- package/dist/report-render/views/page-view.d.ts +20 -0
- package/dist/report-render/views/page-view.js +49 -0
- package/dist/report-render/views/prose.d.ts +28 -0
- package/dist/report-render/views/prose.js +75 -0
- package/dist/report-render/views/references-view.d.ts +20 -0
- package/dist/report-render/views/references-view.js +64 -0
- package/dist/report-render/views/values.d.ts +42 -0
- package/dist/report-render/views/values.js +43 -0
- package/dist/runtime/assemble.d.ts +15 -0
- package/dist/runtime/assemble.js +28 -6
- package/dist/runtime/boot.d.ts +3 -3
- package/dist/runtime/boot.js +15 -4
- package/dist/runtime/dbos.d.ts +4 -4
- package/dist/state/artifacts.d.ts +16 -0
- package/dist/state/artifacts.js +18 -0
- package/dist/state/init.js +54 -0
- package/dist/state/purge-analysis.js +2 -0
- package/dist/state/report-session-state.d.ts +112 -0
- package/dist/state/report-session-state.js +150 -0
- package/dist/state/report-versions.d.ts +129 -0
- package/dist/state/report-versions.js +167 -0
- package/dist/state/snapshot-parse.d.ts +34 -0
- package/dist/state/snapshot-parse.js +82 -0
- package/dist/tasks/data-profile.js +1 -0
- package/dist/tools/ad-hoc-router.d.ts +7 -2
- package/dist/tools/ad-hoc-router.js +2 -1
- package/dist/tools/bio/chembl.js +1 -0
- package/dist/tools/bio/comptox.js +1 -0
- package/dist/tools/bio/drug-gene-interactions.js +1 -0
- package/dist/tools/bio/gene-disease-evidence.js +1 -0
- package/dist/tools/bio/gene-preclinical-profile.js +1 -0
- package/dist/tools/bio/keys.d.ts +3 -2
- package/dist/tools/bio/keys.js +3 -2
- package/dist/tools/bio/lookup-annotation.js +1 -0
- package/dist/tools/bio/opentargets.d.ts +3 -3
- package/dist/tools/bio/opentargets.js +4 -3
- package/dist/tools/bio/pubchem.js +1 -0
- package/dist/tools/bio/search-clinical-trials.js +1 -0
- package/dist/tools/bio/search-faers.js +1 -0
- package/dist/tools/bio/search-geo-datasets.js +1 -0
- package/dist/tools/bio/search-interactions.js +1 -0
- package/dist/tools/bio/target-safety.d.ts +4 -6
- package/dist/tools/bio/target-safety.js +7 -6
- package/dist/tools/define-tool.d.ts +19 -12
- package/dist/tools/define-tool.js +4 -2
- package/dist/tools/display/show-user.js +1 -0
- package/dist/tools/iterate-report.js +2 -0
- package/dist/tools/lib/chembl-client.d.ts +2 -0
- package/dist/tools/lib/chembl-client.js +1 -0
- package/dist/tools/lib/monarch-client.d.ts +42 -0
- package/dist/tools/lib/monarch-client.js +100 -0
- package/dist/tools/report/build-report.js +1 -0
- package/dist/tools/report/mint-preview-url.js +1 -0
- package/dist/tools/report/preview-snapshot.js +1 -0
- package/dist/tools/report/submit-report.js +1 -0
- package/dist/tools/report/version-fs.js +4 -0
- package/dist/tools/report-authoring/authoring-tools.d.ts +241 -0
- package/dist/tools/report-authoring/authoring-tools.js +460 -0
- package/dist/tools/report-session/index.d.ts +8 -0
- package/dist/tools/report-session/index.js +8 -0
- package/dist/tools/report-session/preview-report.d.ts +90 -0
- package/dist/tools/report-session/preview-report.js +245 -0
- package/dist/tools/research/context7-docs.js +2 -0
- package/dist/tools/research/generate-analogy-report.js +1 -0
- package/dist/tools/research/generate-plan.js +7 -2
- package/dist/tools/research/inspect-data-profile.js +4 -0
- package/dist/tools/research/literature-reviewer.js +1 -0
- package/dist/tools/research/search-arxiv.js +1 -0
- package/dist/tools/research/search-github-repos.js +1 -0
- package/dist/tools/research/search-semantic-scholar.js +1 -0
- package/dist/tools/sandbox/list-available-packages.js +23 -0
- package/dist/tools/sandbox/list-available-refs.js +36 -0
- package/dist/tools/sandbox/report-blocker.js +1 -0
- package/dist/tools/sandbox/skills.js +2 -0
- package/dist/tools/workspace/file-stat.js +1 -0
- package/dist/tools/workspace/show-file.js +2 -0
- package/dist/tools/workspace/show-plan.js +1 -0
- package/dist/workflows/execute-analysis.d.ts +5 -1
- package/dist/workflows/execute-analysis.js +5 -5
- package/dist/workflows/execute-target-assessment.d.ts +13 -2
- package/dist/workflows/execute-target-assessment.js +78 -16
- package/dist/workflows/target-assessment/assemblers/index.d.ts +1 -1
- package/dist/workflows/target-assessment/assemblers/index.js +1 -1
- package/dist/workflows/target-assessment/assemblers/literature.d.ts +9 -27
- package/dist/workflows/target-assessment/assemblers/literature.js +32 -48
- package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +17 -9
- package/dist/workflows/target-assessment/assemblers/orchestrator.js +195 -85
- package/dist/workflows/target-assessment/assemblers/safety.d.ts +70 -48
- package/dist/workflows/target-assessment/assemblers/safety.js +125 -42
- package/dist/workflows/target-assessment/collectors/index.d.ts +5 -2
- package/dist/workflows/target-assessment/collectors/index.js +48 -5
- package/dist/workflows/target-assessment/coverage.d.ts +43 -27
- package/dist/workflows/target-assessment/coverage.js +33 -29
- package/dist/workflows/target-assessment/fanout/index.js +1 -0
- package/dist/workflows/target-assessment/investigation/index.d.ts +97 -0
- package/dist/workflows/target-assessment/investigation/index.js +419 -0
- package/dist/workflows/target-assessment/lib/approval-precedents.d.ts +26 -5
- package/dist/workflows/target-assessment/lib/approval-precedents.js +95 -29
- package/dist/workflows/target-assessment/lib/citation-validator.js +1 -1
- package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.d.ts +2 -2
- package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.d.ts +2 -3
- package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.js +0 -4
- package/dist/workflows/target-assessment/lib/compute-derived.d.ts +6 -5
- package/dist/workflows/target-assessment/lib/compute-derived.js +14 -5
- package/dist/workflows/target-assessment/lib/family-complex-supplement.d.ts +5 -6
- package/dist/workflows/target-assessment/lib/family-complex-supplement.js +2 -3
- package/dist/workflows/target-assessment/lib/fda-label-safety.d.ts +72 -0
- package/dist/workflows/target-assessment/lib/fda-label-safety.js +166 -0
- package/dist/workflows/target-assessment/lib/heterodimer-filter.d.ts +0 -9
- package/dist/workflows/target-assessment/lib/heterodimer-filter.js +0 -10
- package/dist/workflows/target-assessment/lib/hpo-organ-map.d.ts +34 -0
- package/dist/workflows/target-assessment/lib/hpo-organ-map.js +63 -0
- package/dist/workflows/target-assessment/lib/impc-organ-map.d.ts +19 -0
- package/dist/workflows/target-assessment/lib/impc-organ-map.js +39 -0
- package/dist/workflows/target-assessment/lib/meddra-organ-map.d.ts +24 -14
- package/dist/workflows/target-assessment/lib/meddra-organ-map.js +59 -47
- package/dist/workflows/target-assessment/lib/recommendation-sanitizer.js +1 -1
- package/dist/workflows/target-assessment/lib/regulatory-actions.d.ts +2 -3
- package/dist/workflows/target-assessment/lib/regulatory-actions.js +33 -4
- package/dist/workflows/target-assessment/lib/safety-corroboration.d.ts +44 -0
- package/dist/workflows/target-assessment/lib/safety-corroboration.js +273 -0
- package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +5 -7
- package/dist/workflows/target-assessment/lib/target-identity-filter.js +5 -7
- package/dist/workflows/target-assessment/metrics.d.ts +1 -6
- package/dist/workflows/target-assessment/metrics.js +1 -13
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +2147 -807
- package/dist/workflows/target-assessment/phase4-assemble.js +19 -19
- package/dist/workflows/target-assessment/phase5-persist.d.ts +20 -21
- package/dist/workflows/target-assessment/phase5-persist.js +56 -173
- package/dist/workflows/target-assessment/schemas.d.ts +46 -1
- package/dist/workflows/target-assessment/schemas.js +19 -0
- package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.d.ts +1 -0
- package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.js +2 -0
- package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +26 -62
- package/dist/workflows/target-assessment/steps/phase2-aggregate.js +0 -4
- package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +27 -154
- package/dist/workflows/target-assessment/steps/phase3-aggregate.js +0 -6
- package/dist/workflows/target-assessment/steps/synthesis/diagnostics.d.ts +0 -6
- package/dist/workflows/target-assessment/steps/synthesis/diagnostics.js +0 -18
- package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.d.ts +1 -0
- package/dist/workflows/target-assessment/synthesis/index.d.ts +52 -5
- package/dist/workflows/target-assessment/synthesis/index.js +4 -1
- package/dist/workflows/target-assessment/synthesis-probes.d.ts +3 -3
- package/package.json +4 -1
|
@@ -7,18 +7,30 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Coverage discipline: every section that depends on enrichment data
|
|
9
9
|
* carries a `coverage` discriminator (`available | queried_no_data |
|
|
10
|
-
* not_loaded`)
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
10
|
+
* not_loaded | filtered`), taken from the shared envelope rather than
|
|
11
|
+
* declared per section. `filtered` says a threshold of ours emptied the
|
|
12
|
+
* section and carries the filter and the count it discarded; a partially
|
|
13
|
+
* filtered `available` section carries `dropped_count`. Sections that fall
|
|
14
|
+
* back to inference also carry an `inference_path`. Aggregate rows preserve
|
|
15
|
+
* their contributing evidence under `evidence: [...]` arrays; rows that
|
|
16
|
+
* assert a liability carry it under `support`.
|
|
16
17
|
*
|
|
17
18
|
* See `TARGET_DOSSIER.md` for the full editorial specification.
|
|
18
19
|
*/
|
|
19
20
|
import { z } from "zod";
|
|
21
|
+
import { OrganSystemSchema } from "./organ-system.js";
|
|
22
|
+
import { SeveritySchema } from "./severity.js";
|
|
20
23
|
// ── Coverage primitives ─────────────────────────────────────────────
|
|
21
|
-
export const CoverageSchema = z.enum(["available", "queried_no_data", "not_loaded"]);
|
|
24
|
+
export const CoverageSchema = z.enum(["available", "queried_no_data", "not_loaded", "filtered"]);
|
|
25
|
+
/**
|
|
26
|
+
* Coverage states a per-row marker can take.
|
|
27
|
+
*
|
|
28
|
+
* `filtered` is a section-level outcome only: a filter that removes a row
|
|
29
|
+
* removes the row, so a row that survived to be read can never report it.
|
|
30
|
+
* Derived from the section vocabulary rather than restated, so the two
|
|
31
|
+
* cannot drift into separate lists.
|
|
32
|
+
*/
|
|
33
|
+
export const RowCoverageSchema = CoverageSchema.exclude(["filtered"]);
|
|
22
34
|
const CoverageError = z.object({
|
|
23
35
|
message: z.string(),
|
|
24
36
|
source: z.string().optional(),
|
|
@@ -27,12 +39,18 @@ const CoverageError = z.object({
|
|
|
27
39
|
* Build a discriminated section schema: when `coverage` is `available`,
|
|
28
40
|
* `data` is required and matches the inner schema. Otherwise `data` is
|
|
29
41
|
* absent and an optional `error` payload may be present.
|
|
42
|
+
*
|
|
43
|
+
* `dropped_count` rides on the `available` branch as well as the `filtered`
|
|
44
|
+
* one because a filter that removes some rows still leaves a section whose
|
|
45
|
+
* list is partial — without it, a section that discarded most of its rows
|
|
46
|
+
* reports a clean `available` and overstates its own completeness.
|
|
30
47
|
*/
|
|
31
48
|
function withCoverage(data) {
|
|
32
49
|
return z.discriminatedUnion("coverage", [
|
|
33
50
|
z.object({
|
|
34
51
|
coverage: z.literal("available"),
|
|
35
52
|
data,
|
|
53
|
+
dropped_count: z.number().int().nonnegative().optional(),
|
|
36
54
|
inference_path: z.string().optional(),
|
|
37
55
|
truncated: z.boolean().optional(),
|
|
38
56
|
}),
|
|
@@ -41,6 +59,12 @@ function withCoverage(data) {
|
|
|
41
59
|
error: CoverageError.optional(),
|
|
42
60
|
inference_path: z.string().optional(),
|
|
43
61
|
}),
|
|
62
|
+
z.object({
|
|
63
|
+
coverage: z.literal("filtered"),
|
|
64
|
+
filter: z.string(),
|
|
65
|
+
dropped_count: z.number().int().nonnegative(),
|
|
66
|
+
inference_path: z.string().optional(),
|
|
67
|
+
}),
|
|
44
68
|
z.object({
|
|
45
69
|
coverage: z.literal("not_loaded"),
|
|
46
70
|
reason: z.string().optional(),
|
|
@@ -61,6 +85,7 @@ export const SECTION_BLURBS = {
|
|
|
61
85
|
export const EvidenceItemSchema = z.object({
|
|
62
86
|
pmid: z.string().optional(),
|
|
63
87
|
doi: z.string().optional(),
|
|
88
|
+
accession: z.string().optional(),
|
|
64
89
|
source: z.string(),
|
|
65
90
|
predicate: z.string().optional(),
|
|
66
91
|
score: z.number().optional(),
|
|
@@ -79,15 +104,58 @@ export const EvidenceItemSchema = z.object({
|
|
|
79
104
|
.optional(),
|
|
80
105
|
});
|
|
81
106
|
const EvidenceList = z.array(EvidenceItemSchema);
|
|
107
|
+
/**
|
|
108
|
+
* Evidence attached to a claim, which must resolve to something a reader can
|
|
109
|
+
* actually go and check. `source` alone names a provenance bucket, not a
|
|
110
|
+
* record, so it does not qualify on its own.
|
|
111
|
+
*
|
|
112
|
+
* The constraint lives here rather than on `EvidenceItemSchema` because that
|
|
113
|
+
* shape is shared with sections whose producers legitimately carry looser
|
|
114
|
+
* evidence; tightening it globally would break them, and loosening the claim
|
|
115
|
+
* to match them would defeat the point.
|
|
116
|
+
*/
|
|
117
|
+
export const ClaimEvidenceSchema = EvidenceItemSchema.refine((e) => Boolean(e.pmid ?? e.doi ?? e.accession ?? e.regulatory_reference), {
|
|
118
|
+
message: "claim evidence needs a locator: pmid, doi, accession, or regulatory_reference",
|
|
119
|
+
});
|
|
120
|
+
/**
|
|
121
|
+
* What backs a claim: either evidence, or a stated reason there is none.
|
|
122
|
+
*
|
|
123
|
+
* Discriminating on `state` — rather than making `evidence` nullable and
|
|
124
|
+
* checking it at runtime — is what makes a scored claim with no evidence
|
|
125
|
+
* unconstructable rather than merely invalid. `unknown` is a complete,
|
|
126
|
+
* first-class outcome: an invariant that is harder to satisfy honestly than
|
|
127
|
+
* dishonestly makes the data worse, not better.
|
|
128
|
+
*/
|
|
129
|
+
export const ClaimSupportSchema = z.discriminatedUnion("state", [
|
|
130
|
+
z.object({
|
|
131
|
+
state: z.literal("scored"),
|
|
132
|
+
evidence: z.array(ClaimEvidenceSchema).min(1),
|
|
133
|
+
}),
|
|
134
|
+
z.object({
|
|
135
|
+
state: z.literal("unknown"),
|
|
136
|
+
reason: z.string().min(1),
|
|
137
|
+
}),
|
|
138
|
+
]);
|
|
139
|
+
/**
|
|
140
|
+
* Attach support to a claim body.
|
|
141
|
+
*
|
|
142
|
+
* The support rides as a field rather than wrapping the body, because these
|
|
143
|
+
* rows *are* the assertion: a reader of an unevidenced liability still needs
|
|
144
|
+
* the liability. Swapping the body out on the unknown branch would leave
|
|
145
|
+
* nothing to read.
|
|
146
|
+
*/
|
|
147
|
+
function withEvidence(body) {
|
|
148
|
+
return body.extend({ support: ClaimSupportSchema });
|
|
149
|
+
}
|
|
82
150
|
// ── §1.1 Entity ─────────────────────────────────────────────────────
|
|
83
151
|
export const EntitySchema = z.object({
|
|
84
152
|
canonicalId: z.string(),
|
|
85
153
|
symbol: z.string(),
|
|
86
154
|
displayName: z.string(),
|
|
87
155
|
/**
|
|
88
|
-
* Cortex's Phase-0 resolver
|
|
89
|
-
*
|
|
90
|
-
*
|
|
156
|
+
* Cortex's Phase-0 resolver only emits `"gene"`. The schema keeps the
|
|
157
|
+
* broader enum so protein-, rna- and complex-level entities are
|
|
158
|
+
* expressible once the resolver supports them.
|
|
91
159
|
*/
|
|
92
160
|
entityType: z.enum(["gene", "protein", "rna", "complex"]),
|
|
93
161
|
ontology: z.string(),
|
|
@@ -101,46 +169,46 @@ export const EntitySchema = z.object({
|
|
|
101
169
|
synonyms: z.array(z.string()).default([]),
|
|
102
170
|
proteinFamily: z.string().optional(),
|
|
103
171
|
});
|
|
104
|
-
// ── §1.2 Summary ────────────────────────────────────────────────────
|
|
105
|
-
export const SummarySchema = z.object({
|
|
106
|
-
total_evidence_items: z.number().int().nonnegative(),
|
|
107
|
-
total_distinct_papers: z.number().int().nonnegative(),
|
|
108
|
-
has_human_evidence: z.boolean(),
|
|
109
|
-
has_clinical_evidence: z.boolean(),
|
|
110
|
-
strongest_strength_label: z.string().nullable(),
|
|
111
|
-
highest_score: z.number().nullable(),
|
|
112
|
-
highest_score_is_conflicted: z.boolean(),
|
|
113
|
-
total_distinct_clinical_trials: z.number().int().nonnegative(),
|
|
114
|
-
});
|
|
115
172
|
// ── §2.1 Liability summary ──────────────────────────────────────────
|
|
116
|
-
|
|
173
|
+
/**
|
|
174
|
+
* The assertion a liability bullet makes, without its support.
|
|
175
|
+
*
|
|
176
|
+
* Named separately because a dropped bullet records the same assertion
|
|
177
|
+
* plus the reason it was dropped, and carries no support of its own.
|
|
178
|
+
*/
|
|
179
|
+
export const LiabilityBulletBodySchema = z.object({
|
|
117
180
|
text: z.string(),
|
|
118
181
|
rationale: z.string(),
|
|
119
182
|
category: z.enum(["fatal_post_market", "class_liability", "off_target_safety", "high_safety_organ_expression", "broad_expression", "other"]),
|
|
120
183
|
});
|
|
121
|
-
export const
|
|
184
|
+
export const LiabilityBulletSchema = withEvidence(LiabilityBulletBodySchema);
|
|
185
|
+
/**
|
|
186
|
+
* Liability bullets removed post-synthesis because they cited a PMID the
|
|
187
|
+
* direction-of-effect auditor found contradicts the bullet's framing.
|
|
188
|
+
* Mirrors `executive_recommendation.key_risks_dropped` — the dropped
|
|
189
|
+
* content is preserved with the reason for an audit trail.
|
|
190
|
+
*/
|
|
191
|
+
export const LiabilityBulletDroppedSchema = LiabilityBulletBodySchema.extend({
|
|
192
|
+
reason: z.string(),
|
|
193
|
+
cited_pmid: z.string().optional(),
|
|
194
|
+
});
|
|
195
|
+
/** `.strict()` rejects the derived counters, which live in the derived sub-tree. */
|
|
196
|
+
export const LiabilitySummarySchema = z
|
|
197
|
+
.object({
|
|
122
198
|
liability_bullets: z.array(LiabilityBulletSchema),
|
|
199
|
+
liability_bullets_dropped: z.array(LiabilityBulletDroppedSchema).optional(),
|
|
123
200
|
modality_recommendation: z.string().nullable(),
|
|
124
|
-
expression_breadth: z.object({
|
|
125
|
-
high_expression_tissue_count: z.number().int().nonnegative(),
|
|
126
|
-
total_assessed_tissues: z.number().int().nonnegative(),
|
|
127
|
-
}),
|
|
128
|
-
class_liability_count: z.number().int().nonnegative(),
|
|
129
|
-
safety_target_off_target_count: z.number().int().nonnegative(),
|
|
130
|
-
off_tissue_risk_organ_count: z.number().int().nonnegative(),
|
|
131
201
|
same_class_drug_count: z.number().int().nonnegative(),
|
|
132
|
-
safety_data_sources_checked: z.number().int().nonnegative(),
|
|
133
202
|
inferred_therapeutic_area: z.string().nullable(),
|
|
134
203
|
no_liabilities_disclosure: z.string().optional(),
|
|
135
|
-
})
|
|
204
|
+
})
|
|
205
|
+
.strict();
|
|
136
206
|
// ── §2.2 Tractability ───────────────────────────────────────────────
|
|
137
207
|
export const TractabilityModalitySchema = z.object({
|
|
138
208
|
/**
|
|
139
|
-
* Phase-4
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* libraries) — keep them in the enum so persisted dossiers don't reject
|
|
143
|
-
* once those land.
|
|
209
|
+
* Phase-4 emits `small_molecule`, `antibody`, and `other_clinical`. The
|
|
210
|
+
* remaining variants belong to the Phase-2 modality fallback (PROTAC,
|
|
211
|
+
* oligonucleotide, peptide libraries).
|
|
144
212
|
*/
|
|
145
213
|
modality: z.enum(["small_molecule", "antibody", "protac", "other_clinical", "oligonucleotide", "peptide"]),
|
|
146
214
|
levels: z.array(z.string()),
|
|
@@ -150,9 +218,31 @@ export const TractabilityModalitySchema = z.object({
|
|
|
150
218
|
approved_drug_ids: z.array(z.string()).optional(),
|
|
151
219
|
note: z.string().optional(),
|
|
152
220
|
});
|
|
153
|
-
|
|
221
|
+
const MOLECULE_TYPE_TO_MODALITY = {
|
|
222
|
+
Protein: ["peptide", "antibody"],
|
|
223
|
+
"Small molecule": ["small_molecule"],
|
|
224
|
+
Oligonucleotide: ["oligonucleotide"],
|
|
225
|
+
};
|
|
226
|
+
export const TractabilitySchema = withCoverage(z
|
|
227
|
+
.object({
|
|
154
228
|
modalities: z.array(TractabilityModalitySchema),
|
|
155
229
|
preferred_modality: z.string().nullable(),
|
|
230
|
+
drug_molecule_types_present: z.array(z.string()).default([]),
|
|
231
|
+
})
|
|
232
|
+
.superRefine((v, ctx) => {
|
|
233
|
+
for (const moleculeType of v.drug_molecule_types_present) {
|
|
234
|
+
const allowed = MOLECULE_TYPE_TO_MODALITY[moleculeType];
|
|
235
|
+
if (!allowed)
|
|
236
|
+
continue; // unknown molecule_type — skip rather than block
|
|
237
|
+
const hasMatching = v.modalities.some((m) => allowed.includes(m.modality) && !m.is_inferred_from_family);
|
|
238
|
+
if (!hasMatching) {
|
|
239
|
+
ctx.addIssue({
|
|
240
|
+
code: "custom",
|
|
241
|
+
message: `drug molecule_type "${moleculeType}" has no matching enumerated modality row`,
|
|
242
|
+
path: ["modalities"],
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
156
246
|
}));
|
|
157
247
|
// ── §2.3 Indications ────────────────────────────────────────────────
|
|
158
248
|
export const IndicationRowSchema = z.object({
|
|
@@ -197,6 +287,62 @@ export const DrugInteractionRowSchema = z.object({
|
|
|
197
287
|
});
|
|
198
288
|
export const DrugInteractionsSchema = withCoverage(z.object({ rows: z.array(DrugInteractionRowSchema) }));
|
|
199
289
|
// ── §2.5 Clinical development ───────────────────────────────────────
|
|
290
|
+
export const ClinicalTrialAttributionBasisSchema = z.object({
|
|
291
|
+
kind: z.enum([
|
|
292
|
+
"mechanism_target_match",
|
|
293
|
+
"known_class_drug",
|
|
294
|
+
"therapeutic_program_match",
|
|
295
|
+
"related_family_target",
|
|
296
|
+
"biomarker_endpoint",
|
|
297
|
+
"condition_only",
|
|
298
|
+
"text_match",
|
|
299
|
+
"manual",
|
|
300
|
+
]),
|
|
301
|
+
source: z.string(),
|
|
302
|
+
excerpt: z.string().optional(),
|
|
303
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
304
|
+
});
|
|
305
|
+
export const ResolvedTrialInterventionSchema = z.object({
|
|
306
|
+
name: z.string(),
|
|
307
|
+
intervention_type: z.string().nullable().optional(),
|
|
308
|
+
chembl_id: z.string().nullable().optional(),
|
|
309
|
+
therapeutic_program_id: z.string().nullable().optional(),
|
|
310
|
+
target_uniprots: z.array(z.string()).default([]),
|
|
311
|
+
resolver_source: z.string(),
|
|
312
|
+
});
|
|
313
|
+
export const ClinicalTrialAttributionSchema = z
|
|
314
|
+
.object({
|
|
315
|
+
relationship: z.enum([
|
|
316
|
+
"direct_modulator",
|
|
317
|
+
"class_modulator",
|
|
318
|
+
"related_family_target",
|
|
319
|
+
"target_biomarker",
|
|
320
|
+
"pathway_biomarker",
|
|
321
|
+
"condition_only",
|
|
322
|
+
"unrelated",
|
|
323
|
+
"unknown",
|
|
324
|
+
]),
|
|
325
|
+
evidence_role: z.enum(["supports_target", "contextual", "excluded"]),
|
|
326
|
+
basis: z.array(ClinicalTrialAttributionBasisSchema).min(1),
|
|
327
|
+
resolved_interventions: z.array(ResolvedTrialInterventionSchema).default([]),
|
|
328
|
+
exclusion_reason: z.string().optional(),
|
|
329
|
+
})
|
|
330
|
+
.superRefine((v, ctx) => {
|
|
331
|
+
if (v.evidence_role !== "supports_target" && !v.exclusion_reason) {
|
|
332
|
+
ctx.addIssue({
|
|
333
|
+
code: "custom",
|
|
334
|
+
message: "contextual/excluded trial evidence requires exclusion_reason",
|
|
335
|
+
path: ["exclusion_reason"],
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
if (v.evidence_role === "supports_target" && v.relationship !== "direct_modulator" && v.relationship !== "class_modulator") {
|
|
339
|
+
ctx.addIssue({
|
|
340
|
+
code: "custom",
|
|
341
|
+
message: "supports_target evidence must be direct_modulator or class_modulator",
|
|
342
|
+
path: ["relationship"],
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
});
|
|
200
346
|
export const ClinicalTrialRowSchema = z.object({
|
|
201
347
|
nct_id: z.string(),
|
|
202
348
|
title: z.string(),
|
|
@@ -206,34 +352,51 @@ export const ClinicalTrialRowSchema = z.object({
|
|
|
206
352
|
start_date: z.string().nullable(),
|
|
207
353
|
completion_date: z.string().nullable(),
|
|
208
354
|
match_confidence: z.enum(["high", "medium", "low", "off_target"]),
|
|
355
|
+
attribution: ClinicalTrialAttributionSchema,
|
|
356
|
+
eligible_for_toxicology_aggregation: z.boolean(),
|
|
209
357
|
});
|
|
210
358
|
export const TrialOutcomeRowSchema = z.object({
|
|
211
359
|
nct_id: z.string(),
|
|
212
|
-
outcome_type: z.enum(["primary", "secondary", "serious_ae", "non_serious_ae", "other_ae"]),
|
|
213
360
|
measure: z.string(),
|
|
361
|
+
outcome_type: z.enum(["primary", "secondary", "other"]),
|
|
214
362
|
description: z.string().optional(),
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
363
|
+
time_frame: z.string().optional(),
|
|
364
|
+
effect: z.discriminatedUnion("kind", [
|
|
365
|
+
z.object({
|
|
366
|
+
kind: z.literal("quantitative"),
|
|
367
|
+
value: z.number(),
|
|
368
|
+
ci_low: z.number().optional(),
|
|
369
|
+
ci_high: z.number().optional(),
|
|
370
|
+
units: z.string(),
|
|
371
|
+
}),
|
|
372
|
+
z.object({
|
|
373
|
+
kind: z.literal("qualitative"),
|
|
374
|
+
direction: z.enum(["increase", "decrease", "no_change"]),
|
|
375
|
+
magnitude_label: z.string(),
|
|
376
|
+
}),
|
|
377
|
+
z.object({
|
|
378
|
+
kind: z.literal("not_extracted"),
|
|
379
|
+
reason: z.enum(["ctgov_no_numeric_result", "ctgov_no_result_groups", "parse_failure"]),
|
|
380
|
+
}),
|
|
381
|
+
]),
|
|
382
|
+
attribution: ClinicalTrialAttributionSchema.optional(),
|
|
383
|
+
eligible_for_toxicology_aggregation: z.boolean().default(false),
|
|
223
384
|
});
|
|
224
|
-
const
|
|
385
|
+
const FailureCategorySchema = z.discriminatedUnion("category", [
|
|
225
386
|
z.object({ category: z.literal("safety"), safety_evidence_excerpt: z.string() }),
|
|
226
387
|
z.object({ category: z.literal("strategic"), category_evidence_excerpt: z.string() }),
|
|
227
388
|
z.object({ category: z.literal("operational"), category_evidence_excerpt: z.string() }),
|
|
228
389
|
z.object({ category: z.literal("efficacy"), category_evidence_excerpt: z.string() }),
|
|
229
390
|
]);
|
|
230
|
-
export const
|
|
391
|
+
export const FailedTrialRowSchema = z
|
|
231
392
|
.object({
|
|
232
393
|
nct_id: z.string(),
|
|
233
394
|
title: z.string(),
|
|
234
395
|
why_stopped: z.string(),
|
|
235
396
|
classifier: z.enum(["rules", "llm"]),
|
|
236
|
-
failure_category:
|
|
397
|
+
failure_category: FailureCategorySchema,
|
|
398
|
+
attribution: ClinicalTrialAttributionSchema,
|
|
399
|
+
eligible_for_toxicology_aggregation: z.boolean(),
|
|
237
400
|
})
|
|
238
401
|
.superRefine((v, ctx) => {
|
|
239
402
|
const excerpt = "safety_evidence_excerpt" in v.failure_category ? v.failure_category.safety_evidence_excerpt : v.failure_category.category_evidence_excerpt;
|
|
@@ -274,31 +437,25 @@ export const ClinicalBenchmarksSchema = z.object({
|
|
|
274
437
|
export const ClinicalDevelopmentSchema = z.object({
|
|
275
438
|
trials: withCoverage(z.object({
|
|
276
439
|
rows: z.array(ClinicalTrialRowSchema),
|
|
277
|
-
|
|
278
|
-
.array(ClinicalTrialRowSchema.extend({
|
|
279
|
-
match_confidence: z.literal("off_target"),
|
|
280
|
-
}))
|
|
281
|
-
.optional(),
|
|
282
|
-
related_receptor: z.string().optional(),
|
|
440
|
+
excluded_rows: z.array(ClinicalTrialRowSchema).default([]),
|
|
283
441
|
selection_criteria: z
|
|
284
442
|
.object({
|
|
285
|
-
derived_from: z.
|
|
286
|
-
min_confidence: z.
|
|
287
|
-
excluded_off_target_count: z.number().int().nonnegative(),
|
|
443
|
+
derived_from: z.string().optional(),
|
|
444
|
+
min_confidence: z.string().optional(),
|
|
445
|
+
excluded_off_target_count: z.number().int().nonnegative().optional(),
|
|
288
446
|
})
|
|
289
447
|
.optional(),
|
|
290
448
|
})),
|
|
291
449
|
outcomes: withCoverage(z.object({ rows: z.array(TrialOutcomeRowSchema) })),
|
|
292
450
|
failed_trials: withCoverage(z.object({
|
|
293
451
|
rows: z.array(FailedTrialRowSchema),
|
|
294
|
-
|
|
295
|
-
related_receptor: z.string().optional(),
|
|
452
|
+
excluded_rows: z.array(FailedTrialRowSchema).default([]),
|
|
296
453
|
})),
|
|
297
|
-
benchmarks:
|
|
454
|
+
benchmarks: ClinicalBenchmarksSchema,
|
|
298
455
|
});
|
|
299
456
|
// ── §2.6 Safety profile ─────────────────────────────────────────────
|
|
300
457
|
export const OrganRiskRowSchema = z.object({
|
|
301
|
-
organ:
|
|
458
|
+
organ: OrganSystemSchema,
|
|
302
459
|
risk_level: z.enum(["high", "medium", "low"]),
|
|
303
460
|
signal_type_count: z.number().int().nonnegative(),
|
|
304
461
|
signals: z.object({
|
|
@@ -309,34 +466,7 @@ export const OrganRiskRowSchema = z.object({
|
|
|
309
466
|
}),
|
|
310
467
|
evidence: EvidenceList,
|
|
311
468
|
});
|
|
312
|
-
|
|
313
|
-
total_reports: z.number().int().nonnegative(),
|
|
314
|
-
fatal_report_count: z.number().int().nonnegative(),
|
|
315
|
-
any_fatal: z.boolean(),
|
|
316
|
-
seriousness_profile: z
|
|
317
|
-
.object({
|
|
318
|
-
death: z.number().int().nonnegative(),
|
|
319
|
-
hospitalization: z.number().int().nonnegative(),
|
|
320
|
-
life_threatening: z.number().int().nonnegative(),
|
|
321
|
-
disabling: z.number().int().nonnegative(),
|
|
322
|
-
congenital_anomaly: z.number().int().nonnegative(),
|
|
323
|
-
other_serious: z.number().int().nonnegative(),
|
|
324
|
-
})
|
|
325
|
-
.optional(),
|
|
326
|
-
top_signals: z.array(z.object({
|
|
327
|
-
meddra_term: z.string(),
|
|
328
|
-
organ: z.string().optional(),
|
|
329
|
-
report_count: z.number().int().nonnegative(),
|
|
330
|
-
proportional_reporting_ratio: z.number().optional(),
|
|
331
|
-
})),
|
|
332
|
-
per_modulator: z.array(z.object({
|
|
333
|
-
modulator: z.string(),
|
|
334
|
-
modulator_id: z.string().nullable(),
|
|
335
|
-
report_count: z.number().int().nonnegative(),
|
|
336
|
-
coverage: CoverageSchema,
|
|
337
|
-
})),
|
|
338
|
-
});
|
|
339
|
-
const SeriousnessAvailableV4Schema = z
|
|
469
|
+
const SeriousnessAvailableSchema = z
|
|
340
470
|
.object({
|
|
341
471
|
coverage: z.literal("available"),
|
|
342
472
|
total_reports: z.number().int().nonnegative(),
|
|
@@ -376,13 +506,13 @@ const SeriousnessAvailableV4Schema = z
|
|
|
376
506
|
});
|
|
377
507
|
}
|
|
378
508
|
});
|
|
379
|
-
const
|
|
509
|
+
const SeriousnessQueriedNoDataSchema = z.object({
|
|
380
510
|
coverage: z.literal("queried_no_data"),
|
|
381
511
|
total_reports: z.number().int().nonnegative(),
|
|
382
512
|
});
|
|
383
|
-
export const
|
|
513
|
+
export const FaersSummarySchema = z.object({
|
|
384
514
|
total_reports: z.number().int().nonnegative(),
|
|
385
|
-
seriousness: z.discriminatedUnion("coverage", [
|
|
515
|
+
seriousness: z.discriminatedUnion("coverage", [SeriousnessAvailableSchema, SeriousnessQueriedNoDataSchema]),
|
|
386
516
|
top_signals: z.array(z.object({
|
|
387
517
|
meddra_term: z.string(),
|
|
388
518
|
organ: z.string().optional(),
|
|
@@ -393,7 +523,7 @@ export const FaersSummaryV4Schema = z.object({
|
|
|
393
523
|
modulator: z.string(),
|
|
394
524
|
modulator_id: z.string().nullable(),
|
|
395
525
|
report_count: z.number().int().nonnegative(),
|
|
396
|
-
coverage:
|
|
526
|
+
coverage: RowCoverageSchema,
|
|
397
527
|
})),
|
|
398
528
|
});
|
|
399
529
|
export const TrialAesSchema = z.object({
|
|
@@ -409,11 +539,7 @@ export const TrialAesSchema = z.object({
|
|
|
409
539
|
organ: z.string().optional(),
|
|
410
540
|
nct_ids: z.array(z.string()),
|
|
411
541
|
})),
|
|
412
|
-
/**
|
|
413
|
-
* Floor pct for non-serious AEs. PR-E lowered the operational value
|
|
414
|
-
* from 5 to 2 so real low-incidence AEs survive the filter; older
|
|
415
|
-
* persisted dossiers with the prior 5% floor still parse.
|
|
416
|
-
*/
|
|
542
|
+
/** Floor pct for non-serious AEs, low enough to keep real low-incidence AEs. */
|
|
417
543
|
non_serious_floor_pct: z.number().nonnegative().default(2),
|
|
418
544
|
/**
|
|
419
545
|
* Count of rows dropped by the catch-all-bucket filter (e.g., "Other"
|
|
@@ -429,28 +555,7 @@ export const TrialAesSchema = z.object({
|
|
|
429
555
|
})
|
|
430
556
|
.optional(),
|
|
431
557
|
});
|
|
432
|
-
|
|
433
|
-
off_target_id: z.string().nullable(),
|
|
434
|
-
off_target_name: z.string(),
|
|
435
|
-
target_class: z.string().optional(),
|
|
436
|
-
pchembl: z.number(),
|
|
437
|
-
is_safety_panel_target: z.boolean(),
|
|
438
|
-
organ_system: z.string().nullable(),
|
|
439
|
-
clinical_consequence: z.string().nullable(),
|
|
440
|
-
selectivity: z.object({
|
|
441
|
-
log_units: z.number(),
|
|
442
|
-
fold: z.number(),
|
|
443
|
-
legacy_alias: z.number().optional(),
|
|
444
|
-
}),
|
|
445
|
-
selectivity_window_below_threshold: z.boolean(),
|
|
446
|
-
evidence: EvidenceList,
|
|
447
|
-
metadata: z
|
|
448
|
-
.object({
|
|
449
|
-
merged_chembl_ids: z.array(z.string()).optional(),
|
|
450
|
-
})
|
|
451
|
-
.optional(),
|
|
452
|
-
});
|
|
453
|
-
const SelectivityV4Schema = z.union([
|
|
558
|
+
const SelectivitySchema = z.union([
|
|
454
559
|
z.object({
|
|
455
560
|
vs_primary_potency: z.object({
|
|
456
561
|
primary_pchembl_used: z.number(),
|
|
@@ -464,32 +569,39 @@ const SelectivityV4Schema = z.union([
|
|
|
464
569
|
reason: z.string().min(1),
|
|
465
570
|
}),
|
|
466
571
|
]);
|
|
467
|
-
|
|
572
|
+
/**
|
|
573
|
+
* The measured facts about an off-target hit, shared by the rows that
|
|
574
|
+
* assert a liability and the rows that record why one was set aside.
|
|
575
|
+
*/
|
|
576
|
+
export const OffTargetRowBodySchema = z.object({
|
|
468
577
|
off_target_id: z.string().nullable(),
|
|
469
578
|
off_target_name: z.string(),
|
|
470
579
|
target_class: z.string().optional(),
|
|
471
580
|
pchembl: z.number(),
|
|
472
581
|
is_safety_panel_target: z.boolean(),
|
|
473
|
-
organ_system:
|
|
582
|
+
organ_system: OrganSystemSchema.nullable(),
|
|
474
583
|
clinical_consequence: z.string().nullable(),
|
|
475
|
-
selectivity:
|
|
584
|
+
selectivity: SelectivitySchema,
|
|
476
585
|
selectivity_window_below_threshold: z.boolean(),
|
|
477
|
-
evidence: EvidenceList,
|
|
478
586
|
metadata: z.object({ merged_chembl_ids: z.array(z.string()).optional() }).optional(),
|
|
479
587
|
});
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
588
|
+
/** The claim's own evidence rides in `support`, so the row carries no second evidence list. */
|
|
589
|
+
export const OffTargetRowSchema = withEvidence(OffTargetRowBodySchema.extend({ relationship: z.literal("off_target") }));
|
|
590
|
+
/**
|
|
591
|
+
* Excluded rows record why something was set aside rather than asserting a
|
|
592
|
+
* liability, so they carry evidence without the claim contract.
|
|
593
|
+
*/
|
|
594
|
+
export const ExcludedOffTargetRowSchema = OffTargetRowBodySchema.extend({
|
|
484
595
|
relationship: z.enum(["intended_co_target", "on_target_self_hit", "obligate_cofactor"]),
|
|
485
596
|
reason: z.string().min(1),
|
|
597
|
+
evidence: EvidenceList,
|
|
486
598
|
});
|
|
487
|
-
export const
|
|
488
|
-
rows: z.array(
|
|
489
|
-
excluded_rows: z.array(
|
|
599
|
+
export const OffTargetPanelSchema = z.object({
|
|
600
|
+
rows: z.array(OffTargetRowSchema),
|
|
601
|
+
excluded_rows: z.array(ExcludedOffTargetRowSchema),
|
|
490
602
|
});
|
|
491
603
|
export const ClassLiabilityOrganSchema = z.object({
|
|
492
|
-
organ:
|
|
604
|
+
organ: OrganSystemSchema,
|
|
493
605
|
drug_count_in_class: z.number().int().nonnegative(),
|
|
494
606
|
drugs_with_signal: z.number().int().nonnegative(),
|
|
495
607
|
signal_fraction: z.number(),
|
|
@@ -509,37 +621,196 @@ export const ClassPrecedentSchema = z.object({
|
|
|
509
621
|
})),
|
|
510
622
|
per_organ: z.array(ClassLiabilityOrganSchema),
|
|
511
623
|
});
|
|
512
|
-
export const SafetyFlagSchema = z.object({
|
|
513
|
-
organ:
|
|
624
|
+
export const SafetyFlagSchema = withEvidence(z.object({
|
|
625
|
+
organ: OrganSystemSchema,
|
|
514
626
|
trail: z.string(),
|
|
515
627
|
mechanism_hypothesis: z.string().nullable().optional(),
|
|
628
|
+
severity: SeveritySchema,
|
|
629
|
+
}));
|
|
630
|
+
export const RegulatoryActionRowSchema = z
|
|
631
|
+
.object({
|
|
632
|
+
drug_chembl_id: z.string(),
|
|
633
|
+
drug_name: z.string(),
|
|
634
|
+
agency: z.enum(["FDA", "EMA", "MHRA", "PMDA", "Health Canada", "TGA"]),
|
|
635
|
+
action_kind: z.enum(["referral", "withdrawal", "indication_restriction", "REMS", "black_box", "DHCP", "safety_communication", "label_warning"]),
|
|
636
|
+
action_date: z.string(),
|
|
637
|
+
finding: z.string(),
|
|
638
|
+
source_url: z.string().url().optional(),
|
|
639
|
+
source_kind: z.enum(["label_warning", "referral", "rems", "withdrawal", "boxed_warning", "safety_communication"]),
|
|
640
|
+
application_number: z.string().optional(),
|
|
641
|
+
label_section: z.string().optional(),
|
|
642
|
+
source_date: z.string().optional(),
|
|
643
|
+
evidence: EvidenceList,
|
|
644
|
+
})
|
|
645
|
+
.superRefine((v, ctx) => {
|
|
646
|
+
if (v.source_kind === "label_warning" && v.action_kind === "safety_communication") {
|
|
647
|
+
ctx.addIssue({
|
|
648
|
+
code: "custom",
|
|
649
|
+
message: "FDA label warnings must not be represented as action_kind:safety_communication",
|
|
650
|
+
path: ["action_kind"],
|
|
651
|
+
});
|
|
652
|
+
}
|
|
516
653
|
});
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
654
|
+
/**
|
|
655
|
+
* One warning fragment from an FDA label, attributed to a canonical organ.
|
|
656
|
+
*
|
|
657
|
+
* The application number plus the label section are what let a reader open the
|
|
658
|
+
* label and find the excerpt again, which is why the row carries both rather
|
|
659
|
+
* than a bare drug name.
|
|
660
|
+
*/
|
|
661
|
+
export const RegulatoryOrganSignalRowSchema = withEvidence(z.object({
|
|
662
|
+
organ: OrganSystemSchema,
|
|
663
|
+
drug_name: z.string(),
|
|
664
|
+
agency: z.literal("FDA"),
|
|
665
|
+
application_number: z.string(),
|
|
666
|
+
label_section: z.string(),
|
|
667
|
+
excerpt: z.string(),
|
|
668
|
+
}));
|
|
669
|
+
export const RegulatoryOrganSignalsSchema = withCoverage(z.object({ rows: z.array(RegulatoryOrganSignalRowSchema) }));
|
|
523
670
|
export const SafetyProfileSchema = z.object({
|
|
524
671
|
organ_rollup: withCoverage(z.object({ rows: z.array(OrganRiskRowSchema) })),
|
|
525
672
|
faers: withCoverage(FaersSummarySchema),
|
|
526
673
|
trial_aes: withCoverage(TrialAesSchema),
|
|
527
|
-
off_target_panel: withCoverage(
|
|
674
|
+
off_target_panel: withCoverage(OffTargetPanelSchema),
|
|
528
675
|
failed_trials_safety_lens: withCoverage(z.object({
|
|
529
676
|
rows: z.array(FailedTrialRowSchema),
|
|
530
|
-
|
|
531
|
-
related_receptor: z.string().optional(),
|
|
677
|
+
excluded_rows: z.array(FailedTrialRowSchema).default([]),
|
|
532
678
|
})),
|
|
533
679
|
class_precedent: withCoverage(ClassPrecedentSchema),
|
|
534
|
-
target_organ_liabilities: z.array(SafetyFlagSchema),
|
|
535
|
-
|
|
680
|
+
target_organ_liabilities: withCoverage(z.object({ rows: z.array(SafetyFlagSchema) })),
|
|
681
|
+
regulatory_actions: withCoverage(z.object({ rows: z.array(RegulatoryActionRowSchema) })).optional(),
|
|
682
|
+
regulatory_organ_signals: RegulatoryOrganSignalsSchema.optional(),
|
|
683
|
+
});
|
|
684
|
+
// ── §2.6a Safety corroboration spine ────────────────────────────────
|
|
685
|
+
/**
|
|
686
|
+
* One source's signal for one organ.
|
|
687
|
+
*
|
|
688
|
+
* The source names itself in a field rather than occupying a field of its own.
|
|
689
|
+
* That is what keeps the contributing set open: a source that did not exist
|
|
690
|
+
* when this shape was written contributes by emitting a record, with no schema
|
|
691
|
+
* edit, no new branch, and no consumer change. A struct with one field per
|
|
692
|
+
* source would instead force a schema change per source — and, because a new
|
|
693
|
+
* source arriving before its field exists has nowhere to land, would invite a
|
|
694
|
+
* second, narrower evidence path alongside this one.
|
|
695
|
+
*
|
|
696
|
+
* `evidence` is claim evidence, so a contribution always resolves to a record a
|
|
697
|
+
* reader can go and check.
|
|
698
|
+
*/
|
|
699
|
+
export const OrganSignalContributionSchema = z.object({
|
|
700
|
+
/** Stable id of the source that produced the signal. */
|
|
701
|
+
source: z.string().min(1),
|
|
702
|
+
/** What this source says about the organ, in the source's own terms. */
|
|
703
|
+
signal: z.string().min(1),
|
|
704
|
+
evidence: ClaimEvidenceSchema,
|
|
705
|
+
});
|
|
706
|
+
/**
|
|
707
|
+
* The corroboration record for one organ: which sources carry a signal for it,
|
|
708
|
+
* how many of them are independent, and what each contributed.
|
|
709
|
+
*
|
|
710
|
+
* `independent_source_count` counts distinct source ids, not contributions — a
|
|
711
|
+
* source carrying three label warnings for one organ is still one source. The
|
|
712
|
+
* claim's evidence is the contributions' evidence, which is why a signal with
|
|
713
|
+
* no locator is never admitted as a contribution in the first place.
|
|
714
|
+
*/
|
|
715
|
+
export const OrganCorroborationRowSchema = withEvidence(z.object({
|
|
716
|
+
organ: OrganSystemSchema,
|
|
717
|
+
contributions: z.array(OrganSignalContributionSchema).min(1),
|
|
718
|
+
corroborating_sources: z.array(z.string().min(1)).min(1),
|
|
719
|
+
independent_source_count: z.number().int().positive(),
|
|
720
|
+
}));
|
|
721
|
+
/**
|
|
722
|
+
* `dropped_count` on this section counts *signals* — the fold's input unit —
|
|
723
|
+
* discarded because their organ did not resolve, because they carried no
|
|
724
|
+
* locator, or because their organ drew fewer than `min_independent_sources`
|
|
725
|
+
* distinct sources.
|
|
726
|
+
*/
|
|
727
|
+
export const SafetyCorroborationSchema = withCoverage(z.object({
|
|
728
|
+
rows: z.array(OrganCorroborationRowSchema),
|
|
729
|
+
/** Every source id that offered at least one signal on this run. */
|
|
730
|
+
sources_considered: z.array(z.string().min(1)),
|
|
731
|
+
/** Distinct sources an organ needed to be reported as corroborated. */
|
|
732
|
+
min_independent_sources: z.number().int().positive(),
|
|
733
|
+
}));
|
|
734
|
+
// ── §2.6b Claim investigation ───────────────────────────────────────
|
|
735
|
+
/**
|
|
736
|
+
* What became of a claim once it was argued against.
|
|
737
|
+
*
|
|
738
|
+
* Four words, no number. A numeric soundness would need a threshold to mean
|
|
739
|
+
* anything, and a threshold on this axis deletes real liabilities: the organs
|
|
740
|
+
* with the thinnest published mechanism are routinely the ones worth the most
|
|
741
|
+
* attention. `undetermined` is a real outcome — the argument ran and settled
|
|
742
|
+
* nothing — not a failure to produce one of the other three.
|
|
743
|
+
*/
|
|
744
|
+
export const ClaimVerdictSchema = z.enum(["upheld", "weakened", "overturned", "undetermined"]);
|
|
745
|
+
/** Which stopping condition ended a claim's investigation. */
|
|
746
|
+
export const InvestigationConvergenceSchema = z.enum(["verdict_terminal", "verdict_settled", "round_bound_reached"]);
|
|
747
|
+
/** How the target is proposed to produce the liability in an organ. */
|
|
748
|
+
export const MechanismProposalSchema = withEvidence(z.object({ statement: z.string().min(1) }));
|
|
749
|
+
/**
|
|
750
|
+
* The case against the claim.
|
|
751
|
+
*
|
|
752
|
+
* Its `support` is the counter-evidence, held to the same contract as the claim
|
|
753
|
+
* it attacks: an objection that cannot cite a record is `unknown` with a reason,
|
|
754
|
+
* which is what stops "no supporting literature was found" being dressed up as a
|
|
755
|
+
* finding of its own.
|
|
756
|
+
*/
|
|
757
|
+
export const ClaimCritiqueSchema = withEvidence(z.object({ objection: z.string().min(1) }));
|
|
758
|
+
/**
|
|
759
|
+
* One organ claim, after interrogation.
|
|
760
|
+
*
|
|
761
|
+
* The row's own `support` backs the **verdict** — the assertion the row exists
|
|
762
|
+
* to make. The mechanism and the critique are nested claims carrying their own
|
|
763
|
+
* support, because they are separate assertions that can independently fail to
|
|
764
|
+
* be evidenced; flattening all three onto one `support` would make it
|
|
765
|
+
* unreadable which of them it was for.
|
|
766
|
+
*
|
|
767
|
+
* `critique` is singular: objections are deduplicated by organ, so a later
|
|
768
|
+
* round's critique replaces the earlier one it was written against, and
|
|
769
|
+
* `rounds_run` says how many passes it took.
|
|
770
|
+
*/
|
|
771
|
+
export const InvestigatedClaimRowSchema = withEvidence(z.object({
|
|
772
|
+
organ: OrganSystemSchema,
|
|
773
|
+
/** Null when no mechanism was proposable from the evidence collected. */
|
|
774
|
+
mechanism: MechanismProposalSchema.nullable(),
|
|
775
|
+
/** Null when the critique step produced no objection worth recording. */
|
|
776
|
+
critique: ClaimCritiqueSchema.nullable(),
|
|
777
|
+
verdict: ClaimVerdictSchema,
|
|
778
|
+
rounds_run: z.number().int().positive(),
|
|
779
|
+
convergence: InvestigationConvergenceSchema,
|
|
780
|
+
}));
|
|
781
|
+
/** Why a candidate organ claim produced no investigated row. */
|
|
782
|
+
export const UninvestigatedClaimReasonSchema = z.enum(["not_corroborated", "exceeded_claim_budget", "investigation_unavailable"]);
|
|
783
|
+
export const UninvestigatedClaimSchema = z.object({
|
|
784
|
+
organ: OrganSystemSchema,
|
|
785
|
+
reason: UninvestigatedClaimReasonSchema,
|
|
786
|
+
detail: z.string().min(1),
|
|
536
787
|
});
|
|
788
|
+
/**
|
|
789
|
+
* The investigation record.
|
|
790
|
+
*
|
|
791
|
+
* `not_investigated` is part of the data rather than a diagnostic beside it: a
|
|
792
|
+
* run that investigated nothing still has something to say, and it is the only
|
|
793
|
+
* thing that case has to say. That is why `rows` may be empty on an `available`
|
|
794
|
+
* section — every candidate that produced no row is named here with its reason.
|
|
795
|
+
*
|
|
796
|
+
* The two bounds ride in the section because they are what a reader needs to
|
|
797
|
+
* know how hard each claim was pushed, and they are configuration, so the value
|
|
798
|
+
* in force is not derivable from the code a reader happens to be looking at.
|
|
799
|
+
*/
|
|
800
|
+
export const ClaimInvestigationSchema = withCoverage(z.object({
|
|
801
|
+
rows: z.array(InvestigatedClaimRowSchema),
|
|
802
|
+
not_investigated: z.array(UninvestigatedClaimSchema),
|
|
803
|
+
/** Propose → critique → re-verify rounds a single claim was allowed. */
|
|
804
|
+
round_bound: z.number().int().positive(),
|
|
805
|
+
/** Corroborated claims this run was allowed to investigate. */
|
|
806
|
+
claim_budget: z.number().int().positive(),
|
|
807
|
+
}));
|
|
537
808
|
// ── §2.7 Off-tissue risk ────────────────────────────────────────────
|
|
538
|
-
export const OffTissueRowSchema = z.object({
|
|
809
|
+
export const OffTissueRowSchema = withEvidence(z.object({
|
|
539
810
|
tissue: z.string(),
|
|
540
|
-
organ:
|
|
811
|
+
organ: OrganSystemSchema,
|
|
541
812
|
tpm: z.number(),
|
|
542
|
-
});
|
|
813
|
+
}));
|
|
543
814
|
export const OffTissueRiskSchema = withCoverage(z.object({ rows: z.array(OffTissueRowSchema) }));
|
|
544
815
|
// ── §3.1–3.6 Reference biology — keyed evidence sections ────────────
|
|
545
816
|
export const KeyedEvidenceRowSchema = z.object({
|
|
@@ -611,19 +882,40 @@ export const PpiPartnerRowSchema = z.object({
|
|
|
611
882
|
});
|
|
612
883
|
export const PpiNetworkSchema = withCoverage(z.object({ partners: z.array(PpiPartnerRowSchema) }));
|
|
613
884
|
// ── §3.9 Normal tissue expression ───────────────────────────────────
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
885
|
+
/** Source and unit travel together, and the normalization note is mandatory. */
|
|
886
|
+
export const NormalTissueExpressionSchema = z
|
|
887
|
+
.object({
|
|
888
|
+
source: z.enum(["gtex", "hpa_consensus", "hpa_rna_tissue"]),
|
|
889
|
+
unit: z.enum(["tpm", "ntpm", "consensus_normalized"]),
|
|
890
|
+
normalization_notes: z.string().min(1),
|
|
891
|
+
rows: z.array(z.object({
|
|
892
|
+
tissue: z.string(),
|
|
893
|
+
value: z.number(),
|
|
894
|
+
percentile_rank: z.number().optional(),
|
|
895
|
+
})),
|
|
896
|
+
})
|
|
897
|
+
.superRefine((v, ctx) => {
|
|
898
|
+
if (v.unit === "tpm" && v.source !== "gtex") {
|
|
899
|
+
ctx.addIssue({
|
|
900
|
+
code: "custom",
|
|
901
|
+
message: `unit:"tpm" is only valid with source:"gtex" (got source:"${v.source}")`,
|
|
902
|
+
path: ["unit"],
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
if (v.unit === "ntpm" && !(v.source === "hpa_rna_tissue" || v.source === "hpa_consensus")) {
|
|
906
|
+
ctx.addIssue({
|
|
907
|
+
code: "custom",
|
|
908
|
+
message: `unit:"ntpm" is only valid with HPA sources`,
|
|
909
|
+
path: ["unit"],
|
|
910
|
+
});
|
|
911
|
+
}
|
|
619
912
|
});
|
|
620
|
-
export const NormalTissueExpressionSchema = withCoverage(z.object({ rows: z.array(NormalTissueRowSchema) }));
|
|
621
913
|
// ── §3.10 Preclinical ───────────────────────────────────────────────
|
|
622
914
|
export const KoPhenotypeSchema = z.object({
|
|
623
915
|
marker_symbol: z.string().nullable(),
|
|
624
916
|
viability: z.string().nullable(),
|
|
625
917
|
sex_dimorphism: z.boolean(),
|
|
626
|
-
organ_systems_with_phenotype: z.array(
|
|
918
|
+
organ_systems_with_phenotype: z.array(OrganSystemSchema),
|
|
627
919
|
top_mp_terms: z.array(z.string()),
|
|
628
920
|
total_phenotype_count: z.number().int().nonnegative(),
|
|
629
921
|
pre_weaning_lethal: z.boolean(),
|
|
@@ -713,90 +1005,8 @@ export const AdditionalEvidenceSchema = withCoverage(z.object({
|
|
|
713
1005
|
})),
|
|
714
1006
|
score_floor: z.number(),
|
|
715
1007
|
}));
|
|
716
|
-
// ──
|
|
717
|
-
|
|
718
|
-
export const TrialOutcomeRowV4Schema = z.object({
|
|
719
|
-
nct_id: z.string(),
|
|
720
|
-
measure: z.string(),
|
|
721
|
-
outcome_type: z.enum(["primary", "secondary", "other"]),
|
|
722
|
-
description: z.string().optional(),
|
|
723
|
-
time_frame: z.string().optional(),
|
|
724
|
-
effect: z.discriminatedUnion("kind", [
|
|
725
|
-
z.object({
|
|
726
|
-
kind: z.literal("quantitative"),
|
|
727
|
-
value: z.number(),
|
|
728
|
-
ci_low: z.number().optional(),
|
|
729
|
-
ci_high: z.number().optional(),
|
|
730
|
-
units: z.string(),
|
|
731
|
-
}),
|
|
732
|
-
z.object({
|
|
733
|
-
kind: z.literal("qualitative"),
|
|
734
|
-
direction: z.enum(["increase", "decrease", "no_change"]),
|
|
735
|
-
magnitude_label: z.string(),
|
|
736
|
-
}),
|
|
737
|
-
z.object({
|
|
738
|
-
kind: z.literal("not_extracted"),
|
|
739
|
-
reason: z.enum(["ctgov_no_numeric_result", "ctgov_no_result_groups", "parse_failure"]),
|
|
740
|
-
}),
|
|
741
|
-
]),
|
|
742
|
-
});
|
|
743
|
-
// Normal tissue expression with source + unit pair and required normalization_notes
|
|
744
|
-
export const NormalTissueExpressionV4Schema = z
|
|
745
|
-
.object({
|
|
746
|
-
source: z.enum(["gtex", "hpa_consensus", "hpa_rna_tissue"]),
|
|
747
|
-
unit: z.enum(["tpm", "ntpm", "consensus_normalized"]),
|
|
748
|
-
normalization_notes: z.string().min(1),
|
|
749
|
-
rows: z.array(z.object({
|
|
750
|
-
tissue: z.string(),
|
|
751
|
-
value: z.number(),
|
|
752
|
-
percentile_rank: z.number().optional(),
|
|
753
|
-
})),
|
|
754
|
-
})
|
|
755
|
-
.superRefine((v, ctx) => {
|
|
756
|
-
if (v.unit === "tpm" && v.source !== "gtex") {
|
|
757
|
-
ctx.addIssue({
|
|
758
|
-
code: "custom",
|
|
759
|
-
message: `unit:"tpm" is only valid with source:"gtex" (got source:"${v.source}")`,
|
|
760
|
-
path: ["unit"],
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
if (v.unit === "ntpm" && !(v.source === "hpa_rna_tissue" || v.source === "hpa_consensus")) {
|
|
764
|
-
ctx.addIssue({
|
|
765
|
-
code: "custom",
|
|
766
|
-
message: `unit:"ntpm" is only valid with HPA sources`,
|
|
767
|
-
path: ["unit"],
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
});
|
|
771
|
-
// Tractability with modality enumeration refinement
|
|
772
|
-
const MOLECULE_TYPE_TO_MODALITY = {
|
|
773
|
-
Protein: ["peptide", "antibody"],
|
|
774
|
-
"Small molecule": ["small_molecule"],
|
|
775
|
-
Oligonucleotide: ["oligonucleotide"],
|
|
776
|
-
};
|
|
777
|
-
export const TractabilityV4Schema = withCoverage(z
|
|
778
|
-
.object({
|
|
779
|
-
modalities: z.array(TractabilityModalitySchema),
|
|
780
|
-
preferred_modality: z.string().nullable(),
|
|
781
|
-
drug_molecule_types_present: z.array(z.string()).default([]),
|
|
782
|
-
})
|
|
783
|
-
.superRefine((v, ctx) => {
|
|
784
|
-
for (const moleculeType of v.drug_molecule_types_present) {
|
|
785
|
-
const allowed = MOLECULE_TYPE_TO_MODALITY[moleculeType];
|
|
786
|
-
if (!allowed)
|
|
787
|
-
continue; // unknown molecule_type — skip rather than block
|
|
788
|
-
const hasMatching = v.modalities.some((m) => allowed.includes(m.modality) && !m.is_inferred_from_family);
|
|
789
|
-
if (!hasMatching) {
|
|
790
|
-
ctx.addIssue({
|
|
791
|
-
code: "custom",
|
|
792
|
-
message: `drug molecule_type "${moleculeType}" has no matching enumerated modality row`,
|
|
793
|
-
path: ["modalities"],
|
|
794
|
-
});
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
}));
|
|
798
|
-
// Discovery trial row with required relevance_basis discriminator
|
|
799
|
-
export const DiscoveryTrialRowV4Schema = z
|
|
1008
|
+
// ── §4.5 Discovery trials ───────────────────────────────────────────
|
|
1009
|
+
export const DiscoveryTrialRowSchema = z
|
|
800
1010
|
.object({
|
|
801
1011
|
nct_id: z.string(),
|
|
802
1012
|
title: z.string(),
|
|
@@ -816,6 +1026,8 @@ export const DiscoveryTrialRowV4Schema = z
|
|
|
816
1026
|
z.object({ kind: z.literal("condition_match"), matched_term: z.string().optional() }),
|
|
817
1027
|
z.object({ kind: z.literal("manual") }),
|
|
818
1028
|
]),
|
|
1029
|
+
attribution: ClinicalTrialAttributionSchema,
|
|
1030
|
+
eligible_for_toxicology_aggregation: z.boolean(),
|
|
819
1031
|
})
|
|
820
1032
|
.superRefine((v, ctx) => {
|
|
821
1033
|
// Low-confidence rows that only match by condition (no drug link) are unreliable;
|
|
@@ -828,66 +1040,11 @@ export const DiscoveryTrialRowV4Schema = z
|
|
|
828
1040
|
});
|
|
829
1041
|
}
|
|
830
1042
|
});
|
|
831
|
-
// ── V4 derived sub-tree ─────────────────────────────────────────────
|
|
832
|
-
// Shape produced by computeDerivedFields(body) and validated at phase-5
|
|
833
|
-
// persist. `.strict()` rejects unknown keys to prevent accidental extras.
|
|
834
|
-
export const DerivedV4Schema = z
|
|
835
|
-
.object({
|
|
836
|
-
summary: z.object({
|
|
837
|
-
has_human_evidence: z.boolean(),
|
|
838
|
-
has_clinical_evidence: z.boolean(),
|
|
839
|
-
total_evidence_items: z.number().int().nonnegative(),
|
|
840
|
-
total_distinct_papers: z.number().int().nonnegative(),
|
|
841
|
-
total_distinct_clinical_trials: z.number().int().nonnegative(),
|
|
842
|
-
strongest_strength_label: z.string().nullable(),
|
|
843
|
-
highest_score: z.number().nullable(),
|
|
844
|
-
highest_score_is_conflicted: z.boolean(),
|
|
845
|
-
}),
|
|
846
|
-
risk_summary: z.object({
|
|
847
|
-
any_fatal_signal: z.union([z.boolean(), z.literal("unknown")]),
|
|
848
|
-
highest_risk_organ: z.string().nullable(),
|
|
849
|
-
off_target_safety_target_hits: z.number().int().nonnegative(),
|
|
850
|
-
class_liability_count: z.number().int().nonnegative(),
|
|
851
|
-
}),
|
|
852
|
-
liability_summary: z.object({
|
|
853
|
-
counts: z.object({
|
|
854
|
-
class_liability_count: z.number().int().nonnegative(),
|
|
855
|
-
safety_target_off_target_count: z.number().int().nonnegative(),
|
|
856
|
-
off_tissue_risk_organ_count: z.number().int().nonnegative(),
|
|
857
|
-
}),
|
|
858
|
-
expression_breadth: z.object({
|
|
859
|
-
total_assessed_tissues: z.number().int().nonnegative(),
|
|
860
|
-
high_expression_tissue_count: z.number().int().nonnegative(),
|
|
861
|
-
}),
|
|
862
|
-
same_class_drug_count: z.number().int().nonnegative(),
|
|
863
|
-
safety_data_sources_checked: z.number().int().nonnegative(),
|
|
864
|
-
}),
|
|
865
|
-
organ_rollup_completeness: z
|
|
866
|
-
.object({
|
|
867
|
-
expected_organs: z.array(z.string()),
|
|
868
|
-
present_organs: z.array(z.string()),
|
|
869
|
-
missing_organs: z.array(z.string()),
|
|
870
|
-
})
|
|
871
|
-
.refine((v) => v.missing_organs.length === 0, {
|
|
872
|
-
message: "missing_organs must be empty for the dossier to ship",
|
|
873
|
-
path: ["missing_organs"],
|
|
874
|
-
}),
|
|
875
|
-
})
|
|
876
|
-
.strict();
|
|
877
|
-
// ── §4.5 Discovery trials ───────────────────────────────────────────
|
|
878
|
-
export const DiscoveryTrialRowSchema = ClinicalTrialRowSchema.extend({
|
|
879
|
-
match_confidence: z.enum(["high", "medium", "low", "off_target"]),
|
|
880
|
-
});
|
|
881
1043
|
export const DiscoveryTrialsSchema = withCoverage(z.object({
|
|
882
|
-
rows: z.array(
|
|
883
|
-
|
|
884
|
-
.array(ClinicalTrialRowSchema.extend({
|
|
885
|
-
match_confidence: z.literal("off_target"),
|
|
886
|
-
}))
|
|
887
|
-
.optional(),
|
|
888
|
-
related_receptor: z.string().optional(),
|
|
1044
|
+
rows: z.array(DiscoveryTrialRowSchema),
|
|
1045
|
+
excluded_rows: z.array(DiscoveryTrialRowSchema).default([]),
|
|
889
1046
|
}));
|
|
890
|
-
// ── §1.0 Executive recommendation
|
|
1047
|
+
// ── §1.0 Executive recommendation ───────────────────────────────────
|
|
891
1048
|
export const ExecutiveRecommendationDataSchema = z.object({
|
|
892
1049
|
disposition: z.enum(["pursue", "conditional", "de_prioritize", "insufficient_evidence"]),
|
|
893
1050
|
confidence: z.enum(["high", "medium", "low"]),
|
|
@@ -901,15 +1058,6 @@ export const ExecutiveRecommendationDataSchema = z.object({
|
|
|
901
1058
|
cited_pmid: z.string().optional(),
|
|
902
1059
|
}))
|
|
903
1060
|
.optional(),
|
|
904
|
-
// Deprecated; kept for one release for backward compat with previously
|
|
905
|
-
// persisted dossiers. New writes use `key_risks_dropped`. v5 will remove.
|
|
906
|
-
key_risks_downgraded: z
|
|
907
|
-
.array(z.object({
|
|
908
|
-
bullet_index: z.number().int().nonnegative(),
|
|
909
|
-
reason: z.string(),
|
|
910
|
-
cited_pmid: z.string().optional(),
|
|
911
|
-
}))
|
|
912
|
-
.optional(),
|
|
913
1061
|
modality_choice: z.object({
|
|
914
1062
|
modality: z.string(),
|
|
915
1063
|
rationale: z.string(),
|
|
@@ -928,7 +1076,7 @@ export const ExecutiveRecommendationDataSchema = z.object({
|
|
|
928
1076
|
}),
|
|
929
1077
|
});
|
|
930
1078
|
export const ExecutiveRecommendationSchema = withCoverage(ExecutiveRecommendationDataSchema);
|
|
931
|
-
// ── §4.6 Synthesis diagnostics
|
|
1079
|
+
// ── §4.6 Synthesis diagnostics ──────────────────────────────────────
|
|
932
1080
|
export const SynthesisDiagnosticRowSchema = z.object({
|
|
933
1081
|
step_id: z.enum(["liability-bullets", "safety-flags-trail", "translational-commentary", "dossier-recommendation"]),
|
|
934
1082
|
model_id: z.string(),
|
|
@@ -946,7 +1094,7 @@ export const SynthesisDiagnosticRowSchema = z.object({
|
|
|
946
1094
|
"relaxed",
|
|
947
1095
|
"skipped",
|
|
948
1096
|
]),
|
|
949
|
-
final_coverage:
|
|
1097
|
+
final_coverage: CoverageSchema.extract(["available", "queried_no_data"]),
|
|
950
1098
|
error_kind: z.enum(["synthesis-too-shallow", "synthesis-unavailable", "voice-violation"]).nullable().optional(),
|
|
951
1099
|
error_message: z.string().nullable().optional(),
|
|
952
1100
|
});
|
|
@@ -1000,11 +1148,18 @@ export const RecommendationAuditSchema = withCoverage(z.object({
|
|
|
1000
1148
|
citations_unresolved: z.array(RecommendationAuditEntrySchema),
|
|
1001
1149
|
non_dossier_citations: z.array(z.object({ token: z.string(), source: z.string() })),
|
|
1002
1150
|
}));
|
|
1003
|
-
// ──
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1151
|
+
// ── §4.8 Quality gates ──────────────────────────────────────────────
|
|
1152
|
+
export const QualityGateStatusSchema = z.object({
|
|
1153
|
+
gate_id: z.string(),
|
|
1154
|
+
status: z.enum(["pass", "warning", "blocked"]),
|
|
1155
|
+
message: z.string(),
|
|
1156
|
+
affected_sections: z.array(z.string()).default([]),
|
|
1157
|
+
unresolved_count: z.number().int().nonnegative().optional(),
|
|
1158
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
1159
|
+
});
|
|
1160
|
+
export const QualityGatesSchema = withCoverage(z.object({ rows: z.array(QualityGateStatusSchema) }));
|
|
1161
|
+
// ── Composite shapes ────────────────────────────────────────────────
|
|
1162
|
+
export const ReferenceBiologyShape = z.object({
|
|
1008
1163
|
therapeutic_area_associations: DiseaseAssociationsSchema,
|
|
1009
1164
|
molecular_interactions: MolecularInteractionsSchema,
|
|
1010
1165
|
biomarker_potential: BiomarkerPotentialSchema,
|
|
@@ -1013,7 +1168,7 @@ const ReferenceBiologyShape = z.object({
|
|
|
1013
1168
|
combination_evidence: CombinationEvidenceSchema,
|
|
1014
1169
|
pathway_context: PathwayContextSchema,
|
|
1015
1170
|
ppi_network: PpiNetworkSchema,
|
|
1016
|
-
normal_tissue_expression: NormalTissueExpressionSchema,
|
|
1171
|
+
normal_tissue_expression: withCoverage(NormalTissueExpressionSchema),
|
|
1017
1172
|
preclinical: PreclinicalProfileSchema,
|
|
1018
1173
|
key_papers: KeyPapersSchema,
|
|
1019
1174
|
});
|
|
@@ -1024,300 +1179,88 @@ const AnalyticsShape = z.object({
|
|
|
1024
1179
|
additional_evidence: AdditionalEvidenceSchema,
|
|
1025
1180
|
discovery_trials: DiscoveryTrialsSchema,
|
|
1026
1181
|
synthesis_diagnostics: SynthesisDiagnosticsSchema,
|
|
1182
|
+
quality_gates: QualityGatesSchema,
|
|
1027
1183
|
recommendation_audit: RecommendationAuditSchema.optional(),
|
|
1028
1184
|
});
|
|
1029
|
-
|
|
1030
|
-
|
|
1185
|
+
// ── Derived sub-tree ────────────────────────────────────────────────
|
|
1186
|
+
//
|
|
1187
|
+
// Shape produced by computeDerivedFields(body) and validated at phase-5
|
|
1188
|
+
// persist. `.strict()` rejects unknown keys to prevent accidental extras.
|
|
1189
|
+
export const DerivedSchema = z
|
|
1190
|
+
.object({
|
|
1191
|
+
summary: z.object({
|
|
1192
|
+
has_human_evidence: z.boolean(),
|
|
1193
|
+
has_clinical_evidence: z.boolean(),
|
|
1194
|
+
total_evidence_items: z.number().int().nonnegative(),
|
|
1195
|
+
total_distinct_papers: z.number().int().nonnegative(),
|
|
1196
|
+
total_distinct_clinical_trials: z.number().int().nonnegative(),
|
|
1197
|
+
strongest_strength_label: z.string().nullable(),
|
|
1198
|
+
highest_score: z.number().nullable(),
|
|
1199
|
+
highest_score_is_conflicted: z.boolean(),
|
|
1200
|
+
}),
|
|
1201
|
+
risk_summary: z.object({
|
|
1202
|
+
any_fatal_signal: z.union([z.boolean(), z.literal("unknown")]),
|
|
1203
|
+
highest_risk_organ: OrganSystemSchema.nullable(),
|
|
1204
|
+
off_target_safety_target_hits: z.number().int().nonnegative(),
|
|
1205
|
+
class_liability_count: z.number().int().nonnegative(),
|
|
1206
|
+
}),
|
|
1207
|
+
liability_summary: z.object({
|
|
1208
|
+
counts: z.object({
|
|
1209
|
+
class_liability_count: z.number().int().nonnegative(),
|
|
1210
|
+
safety_target_off_target_count: z.number().int().nonnegative(),
|
|
1211
|
+
off_tissue_risk_organ_count: z.number().int().nonnegative(),
|
|
1212
|
+
}),
|
|
1213
|
+
expression_breadth: z.object({
|
|
1214
|
+
total_assessed_tissues: z.number().int().nonnegative(),
|
|
1215
|
+
high_expression_tissue_count: z.number().int().nonnegative(),
|
|
1216
|
+
}),
|
|
1217
|
+
same_class_drug_count: z.number().int().nonnegative(),
|
|
1218
|
+
safety_data_sources_checked: z.number().int().nonnegative(),
|
|
1219
|
+
}),
|
|
1220
|
+
organ_rollup_completeness: z
|
|
1221
|
+
.object({
|
|
1222
|
+
expected_organs: z.array(OrganSystemSchema),
|
|
1223
|
+
present_organs: z.array(OrganSystemSchema),
|
|
1224
|
+
missing_organs: z.array(OrganSystemSchema),
|
|
1225
|
+
})
|
|
1226
|
+
.refine((v) => v.missing_organs.length === 0, {
|
|
1227
|
+
message: "missing_organs must be empty for the dossier to ship",
|
|
1228
|
+
path: ["missing_organs"],
|
|
1229
|
+
}),
|
|
1230
|
+
})
|
|
1231
|
+
.strict();
|
|
1232
|
+
// ── Top-level Dossier ───────────────────────────────────────────────
|
|
1233
|
+
export const DossierSchema = z.object({
|
|
1031
1234
|
entity: EntitySchema,
|
|
1032
|
-
|
|
1235
|
+
generated_at: z.string(),
|
|
1033
1236
|
liability_summary: LiabilitySummarySchema,
|
|
1034
1237
|
tractability: TractabilitySchema,
|
|
1035
1238
|
indications: IndicationsSchema,
|
|
1036
1239
|
drug_interactions: DrugInteractionsSchema,
|
|
1037
1240
|
clinical_development: ClinicalDevelopmentSchema,
|
|
1038
1241
|
safety_profile: SafetyProfileSchema,
|
|
1242
|
+
safety_corroboration: SafetyCorroborationSchema,
|
|
1243
|
+
claim_investigation: ClaimInvestigationSchema,
|
|
1039
1244
|
off_tissue_risk: OffTissueRiskSchema,
|
|
1040
|
-
off_target_panel: withCoverage(
|
|
1245
|
+
off_target_panel: withCoverage(OffTargetPanelSchema),
|
|
1041
1246
|
reference_biology: ReferenceBiologyShape,
|
|
1042
|
-
};
|
|
1043
|
-
export const DossierSchema = z.object({
|
|
1044
|
-
schema_version: z.literal("3"),
|
|
1045
|
-
...DossierCommonShape,
|
|
1046
|
-
analytics: AnalyticsShape,
|
|
1047
|
-
executive_recommendation: ExecutiveRecommendationSchema,
|
|
1048
|
-
});
|
|
1049
|
-
// ── V4 section wrappers ─────────────────────────────────────────────
|
|
1050
|
-
// OrganRiskRow V4 — extends v3 with a richer organ enum
|
|
1051
|
-
export const OrganRiskRowV4Schema = OrganRiskRowSchema.extend({
|
|
1052
|
-
organ: z.enum([
|
|
1053
|
-
"cardiac",
|
|
1054
|
-
"hepatic",
|
|
1055
|
-
"cns",
|
|
1056
|
-
"renal",
|
|
1057
|
-
"gi",
|
|
1058
|
-
"pancreas",
|
|
1059
|
-
"endocrine_thyroid",
|
|
1060
|
-
"metabolic",
|
|
1061
|
-
"hematologic",
|
|
1062
|
-
"immune",
|
|
1063
|
-
"respiratory",
|
|
1064
|
-
"reproductive",
|
|
1065
|
-
"dermatologic",
|
|
1066
|
-
"musculoskeletal",
|
|
1067
|
-
"oncology",
|
|
1068
|
-
]),
|
|
1069
|
-
});
|
|
1070
|
-
// Liability bullets removed post-synthesis because they cited a PMID the
|
|
1071
|
-
// direction-of-effect auditor found contradicts the bullet's framing.
|
|
1072
|
-
// Mirrors `executive_recommendation.key_risks_dropped` — the dropped
|
|
1073
|
-
// content is preserved with the reason for an audit trail.
|
|
1074
|
-
export const LiabilityBulletDroppedSchema = LiabilityBulletSchema.extend({
|
|
1075
|
-
reason: z.string(),
|
|
1076
|
-
cited_pmid: z.string().optional(),
|
|
1077
|
-
});
|
|
1078
|
-
// LiabilitySummaryV4Schema — strict() rejects derived counter keys that moved to derived sub-tree
|
|
1079
|
-
export const LiabilitySummaryV4Schema = z
|
|
1080
|
-
.object({
|
|
1081
|
-
liability_bullets: z.array(LiabilityBulletSchema),
|
|
1082
|
-
liability_bullets_dropped: z.array(LiabilityBulletDroppedSchema).optional(),
|
|
1083
|
-
modality_recommendation: z.string().nullable(),
|
|
1084
|
-
same_class_drug_count: z.number().int().nonnegative(),
|
|
1085
|
-
inferred_therapeutic_area: z.string().nullable(),
|
|
1086
|
-
no_liabilities_disclosure: z.string().optional(),
|
|
1087
|
-
})
|
|
1088
|
-
.strict();
|
|
1089
|
-
// ClinicalDevelopmentV4Schema — swaps in V4 outcome and failed-trial rows
|
|
1090
|
-
export const ClinicalDevelopmentV4Schema = z.object({
|
|
1091
|
-
trials: withCoverage(z.object({
|
|
1092
|
-
rows: z.array(ClinicalTrialRowSchema),
|
|
1093
|
-
selection_criteria: z
|
|
1094
|
-
.object({
|
|
1095
|
-
derived_from: z.string().optional(),
|
|
1096
|
-
min_confidence: z.string().optional(),
|
|
1097
|
-
excluded_off_target_count: z.number().int().nonnegative().optional(),
|
|
1098
|
-
})
|
|
1099
|
-
.optional(),
|
|
1100
|
-
})),
|
|
1101
|
-
outcomes: withCoverage(z.object({ rows: z.array(TrialOutcomeRowV4Schema) })),
|
|
1102
|
-
failed_trials: withCoverage(z.object({ rows: z.array(FailedTrialRowV4Schema) })),
|
|
1103
|
-
benchmarks: ClinicalBenchmarksSchema,
|
|
1104
|
-
});
|
|
1105
|
-
const RegulatoryActionRowSchema = z.object({
|
|
1106
|
-
drug_chembl_id: z.string(),
|
|
1107
|
-
drug_name: z.string(),
|
|
1108
|
-
agency: z.enum(["FDA", "EMA", "MHRA", "PMDA", "Health Canada", "TGA"]),
|
|
1109
|
-
action_kind: z.enum(["referral", "withdrawal", "indication_restriction", "REMS", "black_box", "DHCP", "safety_communication", "label_warning"]),
|
|
1110
|
-
action_date: z.string(),
|
|
1111
|
-
finding: z.string(),
|
|
1112
|
-
source_url: z.string().url().optional(),
|
|
1113
|
-
source_kind: z.enum(["label_warning", "referral", "rems", "withdrawal", "boxed_warning", "safety_communication"]).optional(),
|
|
1114
|
-
application_number: z.string().optional(),
|
|
1115
|
-
label_section: z.string().optional(),
|
|
1116
|
-
source_date: z.string().optional(),
|
|
1117
|
-
evidence: EvidenceList,
|
|
1118
|
-
});
|
|
1119
|
-
// SafetyProfileV4Schema — swaps in V4 FAERS / off-target / failed-trial / OrganRiskRow
|
|
1120
|
-
export const SafetyProfileV4Schema = z.object({
|
|
1121
|
-
organ_rollup: withCoverage(z.object({ rows: z.array(OrganRiskRowV4Schema) })),
|
|
1122
|
-
faers: withCoverage(FaersSummaryV4Schema),
|
|
1123
|
-
trial_aes: withCoverage(TrialAesSchema),
|
|
1124
|
-
off_target_panel: withCoverage(OffTargetPanelV4Schema),
|
|
1125
|
-
failed_trials_safety_lens: withCoverage(z.object({ rows: z.array(FailedTrialRowV4Schema) })),
|
|
1126
|
-
class_precedent: withCoverage(ClassPrecedentSchema),
|
|
1127
|
-
target_organ_liabilities: z.array(SafetyFlagSchema),
|
|
1128
|
-
regulatory_actions: withCoverage(z.object({ rows: z.array(RegulatoryActionRowSchema) })).optional(),
|
|
1129
|
-
});
|
|
1130
|
-
// ReferenceBiologyV4Shape — swaps normal_tissue_expression for V4 version
|
|
1131
|
-
export const ReferenceBiologyV4Shape = z.object({
|
|
1132
|
-
...ReferenceBiologyShape.shape,
|
|
1133
|
-
normal_tissue_expression: withCoverage(NormalTissueExpressionV4Schema),
|
|
1134
|
-
});
|
|
1135
|
-
// ── V4 top-level DossierV4Schema ────────────────────────────────────
|
|
1136
|
-
export const DossierV4Schema = z.object({
|
|
1137
|
-
schema_version: z.literal("4"),
|
|
1138
|
-
entity: EntitySchema,
|
|
1139
|
-
generated_at: z.string(),
|
|
1140
|
-
liability_summary: LiabilitySummaryV4Schema,
|
|
1141
|
-
tractability: TractabilityV4Schema,
|
|
1142
|
-
indications: IndicationsSchema,
|
|
1143
|
-
drug_interactions: DrugInteractionsSchema,
|
|
1144
|
-
clinical_development: ClinicalDevelopmentV4Schema,
|
|
1145
|
-
safety_profile: SafetyProfileV4Schema,
|
|
1146
|
-
off_tissue_risk: OffTissueRiskSchema,
|
|
1147
|
-
off_target_panel: withCoverage(OffTargetPanelV4Schema),
|
|
1148
|
-
reference_biology: ReferenceBiologyV4Shape,
|
|
1149
|
-
analytics: AnalyticsShape,
|
|
1150
|
-
executive_recommendation: ExecutiveRecommendationSchema,
|
|
1151
|
-
derived: DerivedV4Schema,
|
|
1152
|
-
});
|
|
1153
|
-
// Body-only v4 shape: everything the Phase-4 assembler is responsible for
|
|
1154
|
-
// building, WITHOUT the derived sub-tree. Phase-5 persist computes derived
|
|
1155
|
-
// from the stamped body and validates the complete dossier against
|
|
1156
|
-
// DossierV4Schema. Keeping Phase4OutputSchema loose prevents an assembler
|
|
1157
|
-
// organ-rollup miss from surfacing as a generic workflow-error instead
|
|
1158
|
-
// of the explicit derived-invariant-violation path in phase5-persist.
|
|
1159
|
-
export const DossierV4BodySchema = DossierV4Schema.omit({ derived: true });
|
|
1160
|
-
// ── V5 evidence attribution and quality gates ────────────────────────
|
|
1161
|
-
export const ClinicalTrialAttributionBasisV5Schema = z.object({
|
|
1162
|
-
kind: z.enum([
|
|
1163
|
-
"mechanism_target_match",
|
|
1164
|
-
"known_class_drug",
|
|
1165
|
-
"therapeutic_program_match",
|
|
1166
|
-
"related_family_target",
|
|
1167
|
-
"biomarker_endpoint",
|
|
1168
|
-
"condition_only",
|
|
1169
|
-
"text_match",
|
|
1170
|
-
"manual",
|
|
1171
|
-
]),
|
|
1172
|
-
source: z.string(),
|
|
1173
|
-
excerpt: z.string().optional(),
|
|
1174
|
-
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
1175
|
-
});
|
|
1176
|
-
export const ResolvedTrialInterventionV5Schema = z.object({
|
|
1177
|
-
name: z.string(),
|
|
1178
|
-
intervention_type: z.string().nullable().optional(),
|
|
1179
|
-
chembl_id: z.string().nullable().optional(),
|
|
1180
|
-
therapeutic_program_id: z.string().nullable().optional(),
|
|
1181
|
-
target_uniprots: z.array(z.string()).default([]),
|
|
1182
|
-
resolver_source: z.string(),
|
|
1183
|
-
});
|
|
1184
|
-
export const ClinicalTrialAttributionV5Schema = z
|
|
1185
|
-
.object({
|
|
1186
|
-
relationship: z.enum([
|
|
1187
|
-
"direct_modulator",
|
|
1188
|
-
"class_modulator",
|
|
1189
|
-
"related_family_target",
|
|
1190
|
-
"target_biomarker",
|
|
1191
|
-
"pathway_biomarker",
|
|
1192
|
-
"condition_only",
|
|
1193
|
-
"unrelated",
|
|
1194
|
-
"unknown",
|
|
1195
|
-
]),
|
|
1196
|
-
evidence_role: z.enum(["supports_target", "contextual", "excluded"]),
|
|
1197
|
-
basis: z.array(ClinicalTrialAttributionBasisV5Schema).min(1),
|
|
1198
|
-
resolved_interventions: z.array(ResolvedTrialInterventionV5Schema).default([]),
|
|
1199
|
-
exclusion_reason: z.string().optional(),
|
|
1200
|
-
})
|
|
1201
|
-
.superRefine((v, ctx) => {
|
|
1202
|
-
if (v.evidence_role !== "supports_target" && !v.exclusion_reason) {
|
|
1203
|
-
ctx.addIssue({
|
|
1204
|
-
code: "custom",
|
|
1205
|
-
message: "contextual/excluded trial evidence requires exclusion_reason",
|
|
1206
|
-
path: ["exclusion_reason"],
|
|
1207
|
-
});
|
|
1208
|
-
}
|
|
1209
|
-
if (v.evidence_role === "supports_target" && v.relationship !== "direct_modulator" && v.relationship !== "class_modulator") {
|
|
1210
|
-
ctx.addIssue({
|
|
1211
|
-
code: "custom",
|
|
1212
|
-
message: "supports_target evidence must be direct_modulator or class_modulator",
|
|
1213
|
-
path: ["relationship"],
|
|
1214
|
-
});
|
|
1215
|
-
}
|
|
1216
|
-
});
|
|
1217
|
-
export const ClinicalTrialRowV5Schema = ClinicalTrialRowSchema.extend({
|
|
1218
|
-
attribution: ClinicalTrialAttributionV5Schema,
|
|
1219
|
-
eligible_for_toxicology_aggregation: z.boolean(),
|
|
1220
|
-
});
|
|
1221
|
-
export const FailedTrialRowV5Schema = FailedTrialRowV4Schema.extend({
|
|
1222
|
-
attribution: ClinicalTrialAttributionV5Schema,
|
|
1223
|
-
eligible_for_toxicology_aggregation: z.boolean(),
|
|
1224
|
-
});
|
|
1225
|
-
export const TrialOutcomeRowV5Schema = TrialOutcomeRowV4Schema.extend({
|
|
1226
|
-
attribution: ClinicalTrialAttributionV5Schema.optional(),
|
|
1227
|
-
eligible_for_toxicology_aggregation: z.boolean().default(false),
|
|
1228
|
-
});
|
|
1229
|
-
export const ClinicalDevelopmentV5Schema = z.object({
|
|
1230
|
-
trials: withCoverage(z.object({
|
|
1231
|
-
rows: z.array(ClinicalTrialRowV5Schema),
|
|
1232
|
-
excluded_rows: z.array(ClinicalTrialRowV5Schema).default([]),
|
|
1233
|
-
selection_criteria: z
|
|
1234
|
-
.object({
|
|
1235
|
-
derived_from: z.string().optional(),
|
|
1236
|
-
min_confidence: z.string().optional(),
|
|
1237
|
-
excluded_off_target_count: z.number().int().nonnegative().optional(),
|
|
1238
|
-
})
|
|
1239
|
-
.optional(),
|
|
1240
|
-
})),
|
|
1241
|
-
outcomes: withCoverage(z.object({ rows: z.array(TrialOutcomeRowV5Schema) })),
|
|
1242
|
-
failed_trials: withCoverage(z.object({
|
|
1243
|
-
rows: z.array(FailedTrialRowV5Schema),
|
|
1244
|
-
excluded_rows: z.array(FailedTrialRowV5Schema).default([]),
|
|
1245
|
-
})),
|
|
1246
|
-
benchmarks: ClinicalBenchmarksSchema,
|
|
1247
|
-
});
|
|
1248
|
-
export const RegulatoryActionRowV5Schema = RegulatoryActionRowSchema.extend({
|
|
1249
|
-
action_kind: z.enum(["referral", "withdrawal", "indication_restriction", "REMS", "black_box", "DHCP", "safety_communication", "label_warning"]),
|
|
1250
|
-
source_kind: z.enum(["label_warning", "referral", "rems", "withdrawal", "boxed_warning", "safety_communication"]),
|
|
1251
|
-
application_number: z.string().optional(),
|
|
1252
|
-
label_section: z.string().optional(),
|
|
1253
|
-
source_date: z.string().optional(),
|
|
1254
|
-
}).superRefine((v, ctx) => {
|
|
1255
|
-
if (v.source_kind === "label_warning" && v.action_kind === "safety_communication") {
|
|
1256
|
-
ctx.addIssue({
|
|
1257
|
-
code: "custom",
|
|
1258
|
-
message: "FDA label warnings must not be represented as action_kind:safety_communication",
|
|
1259
|
-
path: ["action_kind"],
|
|
1260
|
-
});
|
|
1261
|
-
}
|
|
1262
|
-
});
|
|
1263
|
-
export const SafetyProfileV5Schema = SafetyProfileV4Schema.extend({
|
|
1264
|
-
regulatory_actions: withCoverage(z.object({ rows: z.array(RegulatoryActionRowV5Schema) })).optional(),
|
|
1265
|
-
});
|
|
1266
|
-
export const QualityGateStatusSchema = z.object({
|
|
1267
|
-
gate_id: z.string(),
|
|
1268
|
-
status: z.enum(["pass", "warning", "blocked"]),
|
|
1269
|
-
message: z.string(),
|
|
1270
|
-
affected_sections: z.array(z.string()).default([]),
|
|
1271
|
-
unresolved_count: z.number().int().nonnegative().optional(),
|
|
1272
|
-
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
1273
|
-
});
|
|
1274
|
-
export const QualityGatesSchema = withCoverage(z.object({ rows: z.array(QualityGateStatusSchema) }));
|
|
1275
|
-
export const DiscoveryTrialRowV5Schema = DiscoveryTrialRowV4Schema.extend({
|
|
1276
|
-
attribution: ClinicalTrialAttributionV5Schema,
|
|
1277
|
-
eligible_for_toxicology_aggregation: z.boolean(),
|
|
1278
|
-
});
|
|
1279
|
-
export const DiscoveryTrialsV5Schema = withCoverage(z.object({
|
|
1280
|
-
rows: z.array(DiscoveryTrialRowV5Schema),
|
|
1281
|
-
excluded_rows: z.array(DiscoveryTrialRowV5Schema).default([]),
|
|
1282
|
-
}));
|
|
1283
|
-
const AnalyticsV5Shape = AnalyticsShape.extend({
|
|
1284
|
-
discovery_trials: DiscoveryTrialsV5Schema,
|
|
1285
|
-
quality_gates: QualityGatesSchema,
|
|
1286
|
-
recommendation_audit: RecommendationAuditSchema.optional(),
|
|
1287
|
-
});
|
|
1288
|
-
export const DossierV5Schema = z.object({
|
|
1289
|
-
schema_version: z.literal("5"),
|
|
1290
|
-
entity: EntitySchema,
|
|
1291
|
-
generated_at: z.string(),
|
|
1292
|
-
liability_summary: LiabilitySummaryV4Schema,
|
|
1293
|
-
tractability: TractabilityV4Schema,
|
|
1294
|
-
indications: IndicationsSchema,
|
|
1295
|
-
drug_interactions: DrugInteractionsSchema,
|
|
1296
|
-
clinical_development: ClinicalDevelopmentV5Schema,
|
|
1297
|
-
safety_profile: SafetyProfileV5Schema,
|
|
1298
|
-
off_tissue_risk: OffTissueRiskSchema,
|
|
1299
|
-
off_target_panel: withCoverage(OffTargetPanelV4Schema),
|
|
1300
|
-
reference_biology: ReferenceBiologyV4Shape,
|
|
1301
|
-
analytics: AnalyticsV5Shape,
|
|
1302
|
-
executive_recommendation: ExecutiveRecommendationSchema,
|
|
1303
|
-
derived: DerivedV4Schema,
|
|
1304
|
-
});
|
|
1305
|
-
export const DossierV5BodySchema = DossierV5Schema.omit({ derived: true });
|
|
1306
|
-
// Retain the v3 export under a legacy alias for any code that still reads
|
|
1307
|
-
// persisted v3 dossiers (phase4-assemble, tests, integration).
|
|
1308
|
-
export const DossierSchemaV3Legacy = z.object({
|
|
1309
|
-
schema_version: z.literal("3"),
|
|
1310
|
-
...DossierCommonShape,
|
|
1311
1247
|
analytics: AnalyticsShape,
|
|
1312
1248
|
executive_recommendation: ExecutiveRecommendationSchema,
|
|
1249
|
+
derived: DerivedSchema,
|
|
1313
1250
|
});
|
|
1251
|
+
/**
|
|
1252
|
+
* Everything the Phase-4 assembler is responsible for building, without the
|
|
1253
|
+
* derived sub-tree. Phase-5 persist computes derived from the stamped body
|
|
1254
|
+
* and validates the complete dossier against `DossierSchema`.
|
|
1255
|
+
*/
|
|
1256
|
+
export const DossierBodySchema = DossierSchema.omit({ derived: true });
|
|
1314
1257
|
// ── Type guards ─────────────────────────────────────────────────────
|
|
1315
1258
|
export function isDossier(value) {
|
|
1316
|
-
return
|
|
1259
|
+
return DossierSchema.safeParse(value).success;
|
|
1317
1260
|
}
|
|
1318
1261
|
/** JSON Schema export for runtime validation in non-TS environments. */
|
|
1319
1262
|
export function dossierJsonSchema() {
|
|
1320
|
-
return z.toJSONSchema(
|
|
1263
|
+
return z.toJSONSchema(DossierSchema);
|
|
1321
1264
|
}
|
|
1322
1265
|
// ── Progress events ─────────────────────────────────────────────────
|
|
1323
1266
|
export const TargetAssessmentPhaseSchema = z.enum([
|