@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.
Files changed (249) hide show
  1. package/dist/agents/report-builder.d.ts +2 -0
  2. package/dist/agents/report-builder.js +2 -0
  3. package/dist/agents/report-session-agent.d.ts +56 -0
  4. package/dist/agents/report-session-agent.js +72 -0
  5. package/dist/app/report-session-runtime.d.ts +72 -0
  6. package/dist/app/report-session-runtime.js +157 -0
  7. package/dist/app/spawn-report-session.d.ts +85 -0
  8. package/dist/app/spawn-report-session.js +83 -0
  9. package/dist/contracts/chat-events.d.ts +9 -0
  10. package/dist/contracts/index.d.ts +6 -2
  11. package/dist/contracts/index.js +3 -1
  12. package/dist/contracts/organ-system.d.ts +49 -0
  13. package/dist/contracts/organ-system.js +67 -0
  14. package/dist/contracts/report-blocks.d.ts +564 -0
  15. package/dist/contracts/report-blocks.js +117 -0
  16. package/dist/contracts/report-reference.d.ts +459 -0
  17. package/dist/contracts/report-reference.js +256 -0
  18. package/dist/contracts/schemas/chat-events.d.ts +2 -0
  19. package/dist/contracts/schemas/chat-events.js +2 -0
  20. package/dist/contracts/severity.d.ts +15 -0
  21. package/dist/contracts/severity.js +10 -0
  22. package/dist/contracts/target-dossier.d.ts +8675 -12972
  23. package/dist/contracts/target-dossier.js +534 -591
  24. package/dist/data/safety-panel-schema.d.ts +18 -5
  25. package/dist/data/safety-panel-schema.js +4 -4
  26. package/dist/execution/artifact-metadata.js +1 -0
  27. package/dist/execution/report-runner.js +1 -3
  28. package/dist/execution/run-synthesis.js +2 -0
  29. package/dist/index.d.ts +6 -0
  30. package/dist/index.js +19 -0
  31. package/dist/lib/async-utils.d.ts +16 -4
  32. package/dist/lib/async-utils.js +20 -15
  33. package/dist/lib/host-concurrency.js +1 -0
  34. package/dist/loop/__fixtures__/scripted-provider.js +23 -2
  35. package/dist/loop/run-agent.js +58 -10
  36. package/dist/loop/tool-detail.d.ts +40 -0
  37. package/dist/loop/tool-detail.js +35 -3
  38. package/dist/loop/types.d.ts +13 -0
  39. package/dist/memory/thread-history.d.ts +10 -0
  40. package/dist/memory/thread-history.js +12 -1
  41. package/dist/memory/thread-store.d.ts +4 -4
  42. package/dist/memory/thread-store.js +18 -9
  43. package/dist/prompts/report-session.d.ts +1 -0
  44. package/dist/prompts/report-session.js +89 -0
  45. package/dist/prompts/target-assessment/briefs/executive-recommendation.d.ts +1 -1
  46. package/dist/prompts/target-assessment/briefs/executive-recommendation.js +5 -5
  47. package/dist/prompts/target-assessment/briefs/liability-bullets.d.ts +1 -1
  48. package/dist/prompts/target-assessment/briefs/liability-bullets.js +1 -1
  49. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.d.ts +1 -1
  50. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.js +11 -3
  51. package/dist/prompts/target-assessment/investigation/adversarial-critique.d.ts +1 -0
  52. package/dist/prompts/target-assessment/investigation/adversarial-critique.js +67 -0
  53. package/dist/prompts/target-assessment/investigation/claim-reverification.d.ts +1 -0
  54. package/dist/prompts/target-assessment/investigation/claim-reverification.js +49 -0
  55. package/dist/prompts/target-assessment/investigation/index.d.ts +3 -0
  56. package/dist/prompts/target-assessment/investigation/index.js +3 -0
  57. package/dist/prompts/target-assessment/investigation/mechanism-proposal.d.ts +1 -0
  58. package/dist/prompts/target-assessment/investigation/mechanism-proposal.js +49 -0
  59. package/dist/prompts/target-assessment/tox-voice/index.d.ts +1 -1
  60. package/dist/prompts/target-assessment/tox-voice/vocabulary.d.ts +8 -5
  61. package/dist/prompts/target-assessment/tox-voice/vocabulary.js +9 -20
  62. package/dist/providers/__fixtures__/session.d.ts +5 -4
  63. package/dist/providers/__fixtures__/session.js +4 -4
  64. package/dist/providers/ai-sdk.d.ts +77 -0
  65. package/dist/providers/ai-sdk.js +149 -19
  66. package/dist/providers/errors.d.ts +14 -0
  67. package/dist/providers/errors.js +103 -18
  68. package/dist/providers/types.d.ts +16 -0
  69. package/dist/providers/types.js +10 -1
  70. package/dist/report-model/block-walk.d.ts +56 -0
  71. package/dist/report-model/block-walk.js +89 -0
  72. package/dist/report-model/draft-finish.d.ts +60 -0
  73. package/dist/report-model/draft-finish.js +56 -0
  74. package/dist/report-model/draft-operations.d.ts +125 -0
  75. package/dist/report-model/draft-operations.js +341 -0
  76. package/dist/report-model/draft-read.d.ts +59 -0
  77. package/dist/report-model/draft-read.js +109 -0
  78. package/dist/report-model/draft.d.ts +51 -0
  79. package/dist/report-model/draft.js +32 -0
  80. package/dist/report-model/fixture-resolver.d.ts +8 -0
  81. package/dist/report-model/fixture-resolver.js +252 -0
  82. package/dist/report-model/pin-snapshot.d.ts +30 -0
  83. package/dist/report-model/pin-snapshot.js +40 -0
  84. package/dist/report-model/reference-resolver.d.ts +113 -0
  85. package/dist/report-model/reference-resolver.js +60 -0
  86. package/dist/report-model/structural-validation.d.ts +22 -0
  87. package/dist/report-model/structural-validation.js +68 -0
  88. package/dist/report-model/validate.d.ts +72 -0
  89. package/dist/report-model/validate.js +95 -0
  90. package/dist/report-render/chart.d.ts +33 -0
  91. package/dist/report-render/chart.js +538 -0
  92. package/dist/report-render/page.d.ts +127 -0
  93. package/dist/report-render/page.js +307 -0
  94. package/dist/report-render/references.d.ts +32 -0
  95. package/dist/report-render/references.js +44 -0
  96. package/dist/report-render/render.d.ts +25 -0
  97. package/dist/report-render/render.js +125 -0
  98. package/dist/report-render/script-json.d.ts +13 -0
  99. package/dist/report-render/script-json.js +15 -0
  100. package/dist/report-render/types.d.ts +39 -0
  101. package/dist/report-render/types.js +10 -0
  102. package/dist/report-render/value-bridge.d.ts +62 -0
  103. package/dist/report-render/value-bridge.js +82 -0
  104. package/dist/report-render/views/chart-view.d.ts +11 -0
  105. package/dist/report-render/views/chart-view.js +23 -0
  106. package/dist/report-render/views/page-view.d.ts +20 -0
  107. package/dist/report-render/views/page-view.js +49 -0
  108. package/dist/report-render/views/prose.d.ts +28 -0
  109. package/dist/report-render/views/prose.js +75 -0
  110. package/dist/report-render/views/references-view.d.ts +20 -0
  111. package/dist/report-render/views/references-view.js +64 -0
  112. package/dist/report-render/views/values.d.ts +42 -0
  113. package/dist/report-render/views/values.js +43 -0
  114. package/dist/runtime/assemble.d.ts +15 -0
  115. package/dist/runtime/assemble.js +28 -6
  116. package/dist/runtime/boot.d.ts +3 -3
  117. package/dist/runtime/boot.js +15 -4
  118. package/dist/runtime/dbos.d.ts +4 -4
  119. package/dist/state/artifacts.d.ts +16 -0
  120. package/dist/state/artifacts.js +18 -0
  121. package/dist/state/init.js +54 -0
  122. package/dist/state/purge-analysis.js +2 -0
  123. package/dist/state/report-session-state.d.ts +112 -0
  124. package/dist/state/report-session-state.js +150 -0
  125. package/dist/state/report-versions.d.ts +129 -0
  126. package/dist/state/report-versions.js +167 -0
  127. package/dist/state/snapshot-parse.d.ts +34 -0
  128. package/dist/state/snapshot-parse.js +82 -0
  129. package/dist/tasks/data-profile.js +1 -0
  130. package/dist/tools/ad-hoc-router.d.ts +7 -2
  131. package/dist/tools/ad-hoc-router.js +2 -1
  132. package/dist/tools/bio/chembl.js +1 -0
  133. package/dist/tools/bio/comptox.js +1 -0
  134. package/dist/tools/bio/drug-gene-interactions.js +1 -0
  135. package/dist/tools/bio/gene-disease-evidence.js +1 -0
  136. package/dist/tools/bio/gene-preclinical-profile.js +1 -0
  137. package/dist/tools/bio/keys.d.ts +3 -2
  138. package/dist/tools/bio/keys.js +3 -2
  139. package/dist/tools/bio/lookup-annotation.js +1 -0
  140. package/dist/tools/bio/opentargets.d.ts +3 -3
  141. package/dist/tools/bio/opentargets.js +4 -3
  142. package/dist/tools/bio/pubchem.js +1 -0
  143. package/dist/tools/bio/search-clinical-trials.js +1 -0
  144. package/dist/tools/bio/search-faers.js +1 -0
  145. package/dist/tools/bio/search-geo-datasets.js +1 -0
  146. package/dist/tools/bio/search-interactions.js +1 -0
  147. package/dist/tools/bio/target-safety.d.ts +4 -6
  148. package/dist/tools/bio/target-safety.js +7 -6
  149. package/dist/tools/define-tool.d.ts +19 -12
  150. package/dist/tools/define-tool.js +4 -2
  151. package/dist/tools/display/show-user.js +1 -0
  152. package/dist/tools/iterate-report.js +2 -0
  153. package/dist/tools/lib/chembl-client.d.ts +2 -0
  154. package/dist/tools/lib/chembl-client.js +1 -0
  155. package/dist/tools/lib/monarch-client.d.ts +42 -0
  156. package/dist/tools/lib/monarch-client.js +100 -0
  157. package/dist/tools/report/build-report.js +1 -0
  158. package/dist/tools/report/mint-preview-url.js +1 -0
  159. package/dist/tools/report/preview-snapshot.js +1 -0
  160. package/dist/tools/report/submit-report.js +1 -0
  161. package/dist/tools/report/version-fs.js +4 -0
  162. package/dist/tools/report-authoring/authoring-tools.d.ts +241 -0
  163. package/dist/tools/report-authoring/authoring-tools.js +460 -0
  164. package/dist/tools/report-session/index.d.ts +8 -0
  165. package/dist/tools/report-session/index.js +8 -0
  166. package/dist/tools/report-session/preview-report.d.ts +90 -0
  167. package/dist/tools/report-session/preview-report.js +245 -0
  168. package/dist/tools/research/context7-docs.js +2 -0
  169. package/dist/tools/research/generate-analogy-report.js +1 -0
  170. package/dist/tools/research/generate-plan.js +7 -2
  171. package/dist/tools/research/inspect-data-profile.js +4 -0
  172. package/dist/tools/research/literature-reviewer.js +1 -0
  173. package/dist/tools/research/search-arxiv.js +1 -0
  174. package/dist/tools/research/search-github-repos.js +1 -0
  175. package/dist/tools/research/search-semantic-scholar.js +1 -0
  176. package/dist/tools/sandbox/list-available-packages.js +23 -0
  177. package/dist/tools/sandbox/list-available-refs.js +36 -0
  178. package/dist/tools/sandbox/report-blocker.js +1 -0
  179. package/dist/tools/sandbox/skills.js +2 -0
  180. package/dist/tools/workspace/file-stat.js +1 -0
  181. package/dist/tools/workspace/show-file.js +2 -0
  182. package/dist/tools/workspace/show-plan.js +1 -0
  183. package/dist/workflows/execute-analysis.d.ts +5 -1
  184. package/dist/workflows/execute-analysis.js +5 -5
  185. package/dist/workflows/execute-target-assessment.d.ts +13 -2
  186. package/dist/workflows/execute-target-assessment.js +78 -16
  187. package/dist/workflows/target-assessment/assemblers/index.d.ts +1 -1
  188. package/dist/workflows/target-assessment/assemblers/index.js +1 -1
  189. package/dist/workflows/target-assessment/assemblers/literature.d.ts +9 -27
  190. package/dist/workflows/target-assessment/assemblers/literature.js +32 -48
  191. package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +17 -9
  192. package/dist/workflows/target-assessment/assemblers/orchestrator.js +195 -85
  193. package/dist/workflows/target-assessment/assemblers/safety.d.ts +70 -48
  194. package/dist/workflows/target-assessment/assemblers/safety.js +125 -42
  195. package/dist/workflows/target-assessment/collectors/index.d.ts +5 -2
  196. package/dist/workflows/target-assessment/collectors/index.js +48 -5
  197. package/dist/workflows/target-assessment/coverage.d.ts +43 -27
  198. package/dist/workflows/target-assessment/coverage.js +33 -29
  199. package/dist/workflows/target-assessment/fanout/index.js +1 -0
  200. package/dist/workflows/target-assessment/investigation/index.d.ts +97 -0
  201. package/dist/workflows/target-assessment/investigation/index.js +419 -0
  202. package/dist/workflows/target-assessment/lib/approval-precedents.d.ts +26 -5
  203. package/dist/workflows/target-assessment/lib/approval-precedents.js +95 -29
  204. package/dist/workflows/target-assessment/lib/citation-validator.js +1 -1
  205. package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.d.ts +2 -2
  206. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.d.ts +2 -3
  207. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.js +0 -4
  208. package/dist/workflows/target-assessment/lib/compute-derived.d.ts +6 -5
  209. package/dist/workflows/target-assessment/lib/compute-derived.js +14 -5
  210. package/dist/workflows/target-assessment/lib/family-complex-supplement.d.ts +5 -6
  211. package/dist/workflows/target-assessment/lib/family-complex-supplement.js +2 -3
  212. package/dist/workflows/target-assessment/lib/fda-label-safety.d.ts +72 -0
  213. package/dist/workflows/target-assessment/lib/fda-label-safety.js +166 -0
  214. package/dist/workflows/target-assessment/lib/heterodimer-filter.d.ts +0 -9
  215. package/dist/workflows/target-assessment/lib/heterodimer-filter.js +0 -10
  216. package/dist/workflows/target-assessment/lib/hpo-organ-map.d.ts +34 -0
  217. package/dist/workflows/target-assessment/lib/hpo-organ-map.js +63 -0
  218. package/dist/workflows/target-assessment/lib/impc-organ-map.d.ts +19 -0
  219. package/dist/workflows/target-assessment/lib/impc-organ-map.js +39 -0
  220. package/dist/workflows/target-assessment/lib/meddra-organ-map.d.ts +24 -14
  221. package/dist/workflows/target-assessment/lib/meddra-organ-map.js +59 -47
  222. package/dist/workflows/target-assessment/lib/recommendation-sanitizer.js +1 -1
  223. package/dist/workflows/target-assessment/lib/regulatory-actions.d.ts +2 -3
  224. package/dist/workflows/target-assessment/lib/regulatory-actions.js +33 -4
  225. package/dist/workflows/target-assessment/lib/safety-corroboration.d.ts +44 -0
  226. package/dist/workflows/target-assessment/lib/safety-corroboration.js +273 -0
  227. package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +5 -7
  228. package/dist/workflows/target-assessment/lib/target-identity-filter.js +5 -7
  229. package/dist/workflows/target-assessment/metrics.d.ts +1 -6
  230. package/dist/workflows/target-assessment/metrics.js +1 -13
  231. package/dist/workflows/target-assessment/phase4-assemble.d.ts +2147 -807
  232. package/dist/workflows/target-assessment/phase4-assemble.js +19 -19
  233. package/dist/workflows/target-assessment/phase5-persist.d.ts +20 -21
  234. package/dist/workflows/target-assessment/phase5-persist.js +56 -173
  235. package/dist/workflows/target-assessment/schemas.d.ts +46 -1
  236. package/dist/workflows/target-assessment/schemas.js +19 -0
  237. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.d.ts +1 -0
  238. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.js +2 -0
  239. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +26 -62
  240. package/dist/workflows/target-assessment/steps/phase2-aggregate.js +0 -4
  241. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +27 -154
  242. package/dist/workflows/target-assessment/steps/phase3-aggregate.js +0 -6
  243. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.d.ts +0 -6
  244. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.js +0 -18
  245. package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.d.ts +1 -0
  246. package/dist/workflows/target-assessment/synthesis/index.d.ts +52 -5
  247. package/dist/workflows/target-assessment/synthesis/index.js +4 -1
  248. package/dist/workflows/target-assessment/synthesis-probes.d.ts +3 -3
  249. 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`). Sections that fall back to inference also carry an
11
- * `inference_path`. Aggregate rows preserve their contributing evidence
12
- * under `evidence: [...]` arrays.
13
- *
14
- * `DossierSchema` is a single v3 shape — no backwards-compatibility union.
15
- * Old v1/v2 persisted rows will not parse; that is intentional.
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 currently only emits `"gene"`. The schema
89
- * keeps the broader enum so the contract is forward-compatible with
90
- * protein-/rna-/complex-level entities once the resolver supports them.
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
- export const LiabilityBulletSchema = z.object({
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 LiabilitySummarySchema = z.object({
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 currently emits only `small_molecule`, `antibody`, and
140
- * `other_clinical`. The remaining variants are reserved for future
141
- * Phase-2 modality-fallback expansions (PROTAC, oligonucleotide, peptide
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
- export const TractabilitySchema = withCoverage(z.object({
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
- result: z.string().optional(),
216
- });
217
- export const FailedTrialRowSchema = z.object({
218
- nct_id: z.string(),
219
- title: z.string(),
220
- why_stopped: z.string(),
221
- failure_category: z.enum(["safety", "efficacy", "strategic", "other"]),
222
- classifier: z.enum(["rules", "llm"]),
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 FailureCategoryV4Schema = z.discriminatedUnion("category", [
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 FailedTrialRowV4Schema = z
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: FailureCategoryV4Schema,
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
- related_target_trials: z
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.literal("analytics.discovery_trials"),
286
- min_confidence: z.enum(["high", "medium"]),
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
- related_target_trials: z.array(FailedTrialRowSchema).optional(),
295
- related_receptor: z.string().optional(),
452
+ excluded_rows: z.array(FailedTrialRowSchema).default([]),
296
453
  })),
297
- benchmarks: withCoverage(ClinicalBenchmarksSchema),
454
+ benchmarks: ClinicalBenchmarksSchema,
298
455
  });
299
456
  // ── §2.6 Safety profile ─────────────────────────────────────────────
300
457
  export const OrganRiskRowSchema = z.object({
301
- organ: z.enum(["cardiac", "hepatic", "cns", "renal", "gi", "hematologic", "metabolic", "immune", "respiratory"]),
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
- export const FaersSummarySchema = z.object({
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 SeriousnessQueriedNoDataV4Schema = z.object({
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 FaersSummaryV4Schema = z.object({
513
+ export const FaersSummarySchema = z.object({
384
514
  total_reports: z.number().int().nonnegative(),
385
- seriousness: z.discriminatedUnion("coverage", [SeriousnessAvailableV4Schema, SeriousnessQueriedNoDataV4Schema]),
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: CoverageSchema,
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
- export const OffTargetRowSchema = z.object({
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
- const OffTargetRowV4Base = z.object({
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: z.string().nullable(),
582
+ organ_system: OrganSystemSchema.nullable(),
474
583
  clinical_consequence: z.string().nullable(),
475
- selectivity: SelectivityV4Schema,
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
- export const OffTargetRowV4Schema = OffTargetRowV4Base.extend({
481
- relationship: z.literal("off_target"),
482
- });
483
- export const ExcludedOffTargetRowV4Schema = OffTargetRowV4Base.extend({
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 OffTargetPanelV4Schema = z.object({
488
- rows: z.array(OffTargetRowV4Schema),
489
- excluded_rows: z.array(ExcludedOffTargetRowV4Schema),
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: z.string(),
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: z.string(),
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
- export const RiskSummarySchema = z.object({
518
- highest_risk_organ: z.string().nullable(),
519
- off_target_safety_target_hits: z.number().int().nonnegative(),
520
- class_liability_count: z.number().int().nonnegative(),
521
- any_fatal_signal: z.boolean(),
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(z.object({ rows: z.array(OffTargetRowSchema) })),
674
+ off_target_panel: withCoverage(OffTargetPanelSchema),
528
675
  failed_trials_safety_lens: withCoverage(z.object({
529
676
  rows: z.array(FailedTrialRowSchema),
530
- related_target_trials: z.array(FailedTrialRowSchema).optional(),
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
- risk_summary: RiskSummarySchema,
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: z.string(),
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
- export const NormalTissueRowSchema = z.object({
615
- tissue: z.string(),
616
- organ: z.string().optional(),
617
- tpm: z.number(),
618
- is_safety_relevant: z.boolean(),
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(z.string()),
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
- // ── V4 schema refinements ────────────────────────────────────────────
717
- // Trial outcome with required discriminated effect block
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(DiscoveryTrialRowV4Schema),
883
- related_target_trials: z
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 (v2) ──────────────────────────────
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 (v2) ─────────────────────────────────
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: z.enum(["available", "queried_no_data"]),
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
- // ── Top-level Dossier ───────────────────────────────────────────────
1004
- //
1005
- // `DossierSchema` is a single v3 shape. No backwards-compatibility union.
1006
- // Old v1/v2 persisted rows will not parse; that is intentional.
1007
- const ReferenceBiologyShape = z.object({
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
- const DossierCommonShape = {
1030
- generated_at: z.string(),
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
- summary: SummarySchema,
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(z.object({ rows: z.array(OffTargetRowSchema) })),
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 DossierV5Schema.safeParse(value).success;
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(DossierV5Schema);
1263
+ return z.toJSONSchema(DossierSchema);
1321
1264
  }
1322
1265
  // ── Progress events ─────────────────────────────────────────────────
1323
1266
  export const TargetAssessmentPhaseSchema = z.enum([