ictrp-mcp-server 0.1.1 → 0.2.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/CHANGELOG.md +72 -0
- package/README.md +18 -15
- package/README_ZH.md +15 -12
- package/dist/index.d.ts +12 -6
- package/dist/index.js +24 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/sidecar/ictrp_sidecar.py +43 -13
- package/sidecar/vendor/ictrp_mcp/cache/store.py +52 -20
- package/sidecar/vendor/ictrp_mcp/data/normalize.py +75 -0
- package/sidecar/vendor/ictrp_mcp/ictrp/export_guard.py +86 -14
- package/sidecar/vendor/ictrp_mcp/ictrp/session.py +7 -4
- package/sidecar/vendor/ictrp_mcp/provenance.py +134 -39
- package/sidecar/vendor/ictrp_mcp/tools.py +71 -31
- package/sidecar/vendor/pyproject.toml +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,78 @@
|
|
|
3
3
|
Notable changes to the npm package `ictrp-mcp-server`. The Python package
|
|
4
4
|
`ictrp-mcp-service` is versioned separately in `pyproject.toml`.
|
|
5
5
|
|
|
6
|
+
## 0.2.0
|
|
7
|
+
|
|
8
|
+
**Breaking — the completeness warning was counting the wrong unit, and had been
|
|
9
|
+
since 0.1.0.** `parse_reported_total()` read the portal's **record** count out of
|
|
10
|
+
the results page and every later stage treated it as a **trial** count. For `KRAS`
|
|
11
|
+
it reported the export as "short by 361 trials, 29.0%" when the export in fact
|
|
12
|
+
held all 882 trials the portal reported. The number was always a record count:
|
|
13
|
+
the portal prints `1243 records for 882 trials found for: KRAS` in one sentence,
|
|
14
|
+
and only the first number was ever read.
|
|
15
|
+
|
|
16
|
+
Nothing here changes what is retrieved. It changes what is *claimed* about what
|
|
17
|
+
was retrieved.
|
|
18
|
+
|
|
19
|
+
**Fixed**
|
|
20
|
+
|
|
21
|
+
- `parse_reported_counts()` reads both numbers from the accounting sentence, and
|
|
22
|
+
returns `None` for a unit the page does not state rather than substituting the
|
|
23
|
+
other one.
|
|
24
|
+
- Every response now carries `upstream_reported_records` **and**
|
|
25
|
+
`upstream_reported_trials`, plus `completeness` (`trials_complete`,
|
|
26
|
+
`trials_short` or `unknown`), `rows_short_of_trials` and `duplicate_records`.
|
|
27
|
+
`upstream_reported_total`, `records_incomplete` and `estimated_missing` are
|
|
28
|
+
gone; a snapshot written by an older version is read once, converted, and never
|
|
29
|
+
re-emits the old keys.
|
|
30
|
+
- A `trials_short` response — a real shortfall against the portal's own trial
|
|
31
|
+
count — now carries an explicit `shortfall_notice` saying so. That case did not
|
|
32
|
+
occur in any keyword tested.
|
|
33
|
+
- `incompleteness_notice` explains both units and states that the record-count
|
|
34
|
+
difference is duplicate registration. It is still on every response: absence
|
|
35
|
+
from a result set is still not evidence a trial does not exist.
|
|
36
|
+
- The `ictrp_search` docstring and `line_of_therapy_hint` notes no longer claim
|
|
37
|
+
the export is incomplete, which it is not at trial level.
|
|
38
|
+
|
|
39
|
+
**Removed** — the `parse_reported_total()` name is kept as a deprecated alias for
|
|
40
|
+
`parse_reported_counts().records`. It exists because it is easy to reach for
|
|
41
|
+
again, and it warns.
|
|
42
|
+
|
|
43
|
+
**Tests** — `tests/test_live_accounting.py` (new, `-m live`, gated on
|
|
44
|
+
`ICTRP_LIVE=1`) pins the counts read from the live portal for `KRAS`,
|
|
45
|
+
`pancreatic` and `ChiCTR`, and asserts an export's rows equal the portal's trial
|
|
46
|
+
count. Two tests that asserted the old behaviour were replaced rather than
|
|
47
|
+
deleted; they are listed in `docs/VERIFICATION_29PCT.md`.
|
|
48
|
+
|
|
49
|
+
**Documentation** — every published table of shortfalls is corrected.
|
|
50
|
+
`docs/MEASUREMENTS.md` §2 keeps the withdrawn "8 missing records" finding with
|
|
51
|
+
the failed re-measurement beside it.
|
|
52
|
+
|
|
53
|
+
## 0.1.2
|
|
54
|
+
|
|
55
|
+
**Default search view is now the clinician-triage view.** `ictrp_search` used to
|
|
56
|
+
return the 10 most common columns; it now returns the columns a clinician needs
|
|
57
|
+
to triage a target: title and scientific title, sponsor and secondary sponsor,
|
|
58
|
+
contact (PI) name/affiliation/email, recruitment status, phase, registration
|
|
59
|
+
date, condition, countries, age bounds and gender, target size, and a
|
|
60
|
+
`line_of_therapy_hint`. A new derived field, `line_of_therapy_hint`, is a
|
|
61
|
+
non-authoritative extraction of treatment line from the free-text
|
|
62
|
+
inclusion/exclusion criteria and the scientific title. It returns
|
|
63
|
+
`first-line`/`second-line`/`third-line`/`fourth-line`/`later-line`, or `null`
|
|
64
|
+
when nothing is stated -- `null` is documented as "not stated", never as
|
|
65
|
+
"first-line", because the export is incomplete and the column does not exist.
|
|
66
|
+
|
|
67
|
+
**Line-hint fix** — the hint missed trials whose line is stated only as a number
|
|
68
|
+
or only in the scientific title (e.g. "Received >=2 Prior Lines of Therapy"). It
|
|
69
|
+
now scans the scientific title and recognizes numeric line statements; a minimum
|
|
70
|
+
of N lines resolves to `first-line` only when N<=1, otherwise `later-line`.
|
|
71
|
+
|
|
72
|
+
Documented structural limits surfaced by the wider view: ICTRP has no "PI" and
|
|
73
|
+
no "participating-hospital list" column -- sponsor/PI come from
|
|
74
|
+
`primary_sponsor` + `contact_*`, and "participating hospitals" degrades to
|
|
75
|
+
`countries` + `contact_affiliation`. CT.gov `exclusion_criteria` is frequently
|
|
76
|
+
blank upstream, which is a source gap, not a parse failure.
|
|
77
|
+
|
|
6
78
|
## 0.1.1
|
|
7
79
|
|
|
8
80
|
**Retry on transient stalls** — the search POST intermittently stalls past a
|
package/README.md
CHANGED
|
@@ -106,30 +106,33 @@ single-registry search would miss.
|
|
|
106
106
|
|
|
107
107
|
## The finding that shapes every response
|
|
108
108
|
|
|
109
|
-
**The ICTRP CSV export
|
|
110
|
-
the
|
|
109
|
+
**The ICTRP CSV export gives trials, not records.** The portal states two different
|
|
110
|
+
counts on the results page, and they must not be compared to the same thing:
|
|
111
111
|
|
|
112
|
-
| Query |
|
|
112
|
+
| Query | Records the portal reports | Trials the portal reports | Rows the CSV holds |
|
|
113
113
|
|---|---|---|---|
|
|
114
|
-
| `ChiCTR` | 14,197 | 14,147 |
|
|
115
|
-
| `pancreatic` | 10,354 | 9,273 |
|
|
116
|
-
| `
|
|
117
|
-
| `KRAS` | 1,243 | 882 | **29.0%** |
|
|
114
|
+
| `ChiCTR` | 14,197 | 14,147 | 14,147 |
|
|
115
|
+
| `pancreatic` | 10,354 | 9,273 | 9,273 |
|
|
116
|
+
| `KRAS` | 1,243 | 882 | 882 |
|
|
118
117
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
118
|
+
Rows match **trials** on every keyword tested. Rows do **not** match records: a
|
|
119
|
+
trial registered in several registries is counted once per registration, so the
|
|
120
|
+
gap between the two numbers is duplicate registration of the same trial, not
|
|
121
|
+
missing trials. For `KRAS` that is 361 records.
|
|
122
|
+
|
|
123
|
+
What the export genuinely does lose: one row per trial, keeping the earliest
|
|
124
|
+
registration; an essentially empty `results_*` block; and a `Secondary ID`
|
|
125
|
+
populated for only about 73% of rows, so the record-to-group mapping cannot be
|
|
126
|
+
rebuilt from the CSV alone.
|
|
124
127
|
|
|
125
128
|
So this server never presents retrieved counts as match counts:
|
|
126
129
|
|
|
127
130
|
- A failure raises; it never returns zero rows. An empty list would be
|
|
128
131
|
indistinguishable from "no such trial exists", which is exactly the mistake
|
|
129
132
|
this package exists to prevent.
|
|
130
|
-
- There is no `total_matched`. There is `rows_returned`,
|
|
131
|
-
|
|
132
|
-
- Every response carries `
|
|
133
|
+
- There is no `total_matched`. There is `rows_returned`, plus the portal's two
|
|
134
|
+
counts kept under their own names, and they are never merged.
|
|
135
|
+
- Every response carries `completeness` and an `incompleteness_notice`.
|
|
133
136
|
|
|
134
137
|
## Environment variables
|
|
135
138
|
|
package/README_ZH.md
CHANGED
|
@@ -98,26 +98,29 @@ ChiCTR 1)——正是只查单一注册库会漏掉的情形。
|
|
|
98
98
|
|
|
99
99
|
## 决定每个响应的那个发现
|
|
100
100
|
|
|
101
|
-
**ICTRP 的 CSV
|
|
101
|
+
**ICTRP 的 CSV 导出给的是试验,不是记录。** 门户的结果页上印着两个不同的数,
|
|
102
|
+
拿它们去比同一样东西就会错:
|
|
102
103
|
|
|
103
|
-
| 关键词 |
|
|
104
|
+
| 关键词 | 门户报告的记录数 | 门户报告的试验数 | CSV 实际给出的行数 |
|
|
104
105
|
|---|---|---|---|
|
|
105
|
-
| `ChiCTR` | 14,197 | 14,147 |
|
|
106
|
-
| `pancreatic` | 10,354 | 9,273 |
|
|
107
|
-
| `
|
|
108
|
-
| `KRAS` | 1,243 | 882 | **29.0%** |
|
|
106
|
+
| `ChiCTR` | 14,197 | 14,147 | 14,147 |
|
|
107
|
+
| `pancreatic` | 10,354 | 9,273 | 9,273 |
|
|
108
|
+
| `KRAS` | 1,243 | 882 | 882 |
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
行数对得上**试验数**,实测的每个关键词都对得上。行数对不上**记录数**:一个试验
|
|
111
|
+
被登记进多个注册库就会被逐个登记数一遍,所以这两个数的差额是同一个试验的重复
|
|
112
|
+
登记,不是缺失的试验。`KRAS` 的差额是 361 条记录。
|
|
113
|
+
|
|
114
|
+
导出真正丢掉的东西:一个试验一行,只留最早那次登记;`results_*` 整块基本为空;
|
|
115
|
+
`Secondary ID` 只有约 73% 的行有值,光靠 CSV 还原不出「记录→归组」的映射。
|
|
113
116
|
|
|
114
117
|
所以本服务**从不把取回的条数当作命中数**来呈现:
|
|
115
118
|
|
|
116
119
|
- 失败会抛错,绝不返回零行。一个空列表与「不存在这样的试验」无法区分,
|
|
117
120
|
而后者恰恰是本包存在的意义所在——防止这个误判。
|
|
118
|
-
- 不存在 `total_matched`。只有 `rows_returned
|
|
119
|
-
|
|
120
|
-
- 每个响应都带 `
|
|
121
|
+
- 不存在 `total_matched`。只有 `rows_returned`,以及各用各的名字保存的门户两个
|
|
122
|
+
计数,两者永不合并。
|
|
123
|
+
- 每个响应都带 `completeness` 与 `incompleteness_notice`。
|
|
121
124
|
|
|
122
125
|
## 环境变量
|
|
123
126
|
|
package/dist/index.d.ts
CHANGED
|
@@ -4,15 +4,21 @@
|
|
|
4
4
|
*
|
|
5
5
|
* This file is the MCP surface and nothing else. It validates arguments,
|
|
6
6
|
* ensures the Python sidecar is up, forwards the call, and shapes the result.
|
|
7
|
-
* All domain logic -- the postback chain, the
|
|
7
|
+
* All domain logic -- the postback chain, the completeness accounting, the
|
|
8
8
|
* local query engine -- lives in Python, where it is tested.
|
|
9
9
|
*
|
|
10
10
|
* The one rule that shapes every handler here: **a failure must never be
|
|
11
|
-
* presented as a successful empty result.**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* presented as a successful empty result.** An export that cannot be validated
|
|
12
|
+
* is refused outright rather than handed back as zero rows, so an empty answer
|
|
13
|
+
* and a failed answer are never confused. Failures are therefore reported with
|
|
14
|
+
* `isError: true` and a machine-readable `error_code`.
|
|
15
|
+
*
|
|
16
|
+
* A second rule is about units. The ICTRP portal reports two different counts --
|
|
17
|
+
* registry RECORDS, and TRIALS after records pointing at each other via
|
|
18
|
+
* Secondary ID are grouped. This service exports one row per trial, so its row
|
|
19
|
+
* count is comparable to the portal's trial count and never to its record
|
|
20
|
+
* count. Comparing them is what made 0.1.2 report a shortfall of up to 29% on
|
|
21
|
+
* data that was complete; see docs/VERIFICATION_29PCT.md.
|
|
16
22
|
*/
|
|
17
23
|
import { toErrorText } from "./runtime/sidecar-client.js";
|
|
18
24
|
export { toErrorText };
|
package/dist/index.js
CHANGED
|
@@ -4,15 +4,21 @@
|
|
|
4
4
|
*
|
|
5
5
|
* This file is the MCP surface and nothing else. It validates arguments,
|
|
6
6
|
* ensures the Python sidecar is up, forwards the call, and shapes the result.
|
|
7
|
-
* All domain logic -- the postback chain, the
|
|
7
|
+
* All domain logic -- the postback chain, the completeness accounting, the
|
|
8
8
|
* local query engine -- lives in Python, where it is tested.
|
|
9
9
|
*
|
|
10
10
|
* The one rule that shapes every handler here: **a failure must never be
|
|
11
|
-
* presented as a successful empty result.**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* presented as a successful empty result.** An export that cannot be validated
|
|
12
|
+
* is refused outright rather than handed back as zero rows, so an empty answer
|
|
13
|
+
* and a failed answer are never confused. Failures are therefore reported with
|
|
14
|
+
* `isError: true` and a machine-readable `error_code`.
|
|
15
|
+
*
|
|
16
|
+
* A second rule is about units. The ICTRP portal reports two different counts --
|
|
17
|
+
* registry RECORDS, and TRIALS after records pointing at each other via
|
|
18
|
+
* Secondary ID are grouped. This service exports one row per trial, so its row
|
|
19
|
+
* count is comparable to the portal's trial count and never to its record
|
|
20
|
+
* count. Comparing them is what made 0.1.2 report a shortfall of up to 29% on
|
|
21
|
+
* data that was complete; see docs/VERIFICATION_29PCT.md.
|
|
16
22
|
*/
|
|
17
23
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
18
24
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -21,16 +27,22 @@ import { getEnvReport } from "./runtime/env-probe.js";
|
|
|
21
27
|
import { callSidecar, pingSidecar, SEARCH_TIMEOUT_MS, toErrorText, } from "./runtime/sidecar-client.js";
|
|
22
28
|
import { ensureSidecar, installShutdownHooks, sidecarLogTail } from "./runtime/supervisor.js";
|
|
23
29
|
const SERVER_NAME = "ictrp-mcp-server";
|
|
24
|
-
const SERVER_VERSION = "0.
|
|
30
|
+
const SERVER_VERSION = "0.2.0";
|
|
25
31
|
/**
|
|
26
32
|
* Attached to every payload that returns rows.
|
|
27
33
|
*
|
|
28
|
-
* This is not boilerplate. A model that receives 12 rows and no
|
|
29
|
-
* say "there are 12 trials"
|
|
30
|
-
*
|
|
34
|
+
* This is not boilerplate. A model that receives 12 rows and no explanation of
|
|
35
|
+
* what they count will say "there are 12 trials" and stop there. What it must
|
|
36
|
+
* not do is treat a missing trial as a trial that does not exist, or compare
|
|
37
|
+
* the row count against the portal's record count -- those are different units,
|
|
38
|
+
* and the gap between them is duplicate registration, not missing data.
|
|
31
39
|
*/
|
|
32
|
-
const INCOMPLETENESS_NOTICE = "
|
|
33
|
-
"
|
|
40
|
+
const INCOMPLETENESS_NOTICE = "The ICTRP portal reports two counts: RECORDS (registry rows, one trial may be " +
|
|
41
|
+
"registered in several registries) and TRIALS (after the portal groups records " +
|
|
42
|
+
"that reference each other via Secondary ID). This export has one row per TRIAL, " +
|
|
43
|
+
"so its row count is comparable to the portal's trial count, not its record " +
|
|
44
|
+
"count; where the two differ, the difference is duplicate registration records, " +
|
|
45
|
+
"not missing trials. A trial absent from this set is not established, not absent.";
|
|
34
46
|
const ATTRIBUTION_NOTICE = "Source: WHO ICTRP. Attribute as 'WHO ICTRP' and display the processing date. " +
|
|
35
47
|
"Chinese-registry records must be attributed as 'ChiCTR via WHO ICTRP'.";
|
|
36
48
|
const FILTER_SCHEMA = {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EACL,qBAAqB,EACrB,sBAAsB,GAEvB,MAAM,oCAAoC,CAAC;AAE5C,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EACL,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,WAAW,GAEZ,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAE9F,MAAM,WAAW,GAAG,kBAAkB,CAAC;AACvC,MAAM,cAAc,GAAG,OAAO,CAAC;AAE/B;;;;;;;;GAQG;AACH,MAAM,qBAAqB,GACzB,gFAAgF;IAChF,gFAAgF;IAChF,kFAAkF;IAClF,6EAA6E;IAC7E,iFAAiF;IACjF,kFAAkF,CAAC;AAErF,MAAM,kBAAkB,GACtB,+EAA+E;IAC/E,wEAAwE,CAAC;AAE3E,MAAM,aAAa,GAAG;IACpB,IAAI,EAAE,OAAO;IACb,WAAW,EACT,8EAA8E;IAChF,KAAK,EAAE;QACL,IAAI,EAAE,QAAQ;QACd,QAAQ,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC;QACzB,UAAU,EAAE;YACV,KAAK,EAAE;gBACL,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,0FAA0F;oBAC1F,6DAA6D;aAChE;YACD,EAAE,EAAE;gBACF,IAAI,EAAE,QAAQ;gBACd,IAAI,EAAE;oBACJ,IAAI;oBACJ,IAAI;oBACJ,UAAU;oBACV,cAAc;oBACd,IAAI;oBACJ,QAAQ;oBACR,IAAI;oBACJ,KAAK;oBACL,IAAI;oBACJ,KAAK;oBACL,QAAQ;oBACR,YAAY;oBACZ,SAAS;oBACT,aAAa;iBACd;aACF;YACD,KAAK,EAAE;gBACL,WAAW,EAAE,sDAAsD;aACpE;SACF;KACF;CACO,CAAC;AAEX,MAAM,KAAK,GAAW;IACpB;QACE,IAAI,EAAE,cAAc;QACpB,WAAW,EACT,iFAAiF;YACjF,wFAAwF;YACxF,qFAAqF;YACrF,uFAAuF;YACvF,yFAAyF;YACzF,8FAA8F;QAChG,WAAW,EAAE;YACX,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE;gBACV,OAAO,EAAE;oBACP,IAAI,EAAE,QAAQ;oBACd,WAAW,EAAE,8DAA8D;iBAC5E;gBACD,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,0CAA0C,EAAE;gBAClF,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,0BAA0B,EAAE;gBACnE,OAAO,EAAE;oBACP,IAAI,EAAE,SAAS;oBACf,WAAW,EAAE,8EAA8E;iBAC5F;aACF;YACD,QAAQ,EAAE,CAAC,SAAS,CAAC;SACtB;KACF;IACD;QACE,IAAI,EAAE,cAAc;QACpB,WAAW,EACT,iEAAiE;YACjE,0FAA0F;YAC1F,2EAA2E;QAC7E,WAAW,EAAE;YACX,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE;gBACV,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBAC1B,OAAO,EAAE,aAAa;gBACtB,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,mBAAmB,EAAE;gBAC7D,UAAU,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,iCAAiC,EAAE;gBAC/E,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,6BAA6B,EAAE;gBACrE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,0BAA0B,EAAE;aACpE;YACD,QAAQ,EAAE,CAAC,QAAQ,CAAC;SACrB;KACF;IACD;QACE,IAAI,EAAE,mBAAmB;QACzB,WAAW,EACT,0FAA0F;YAC1F,0FAA0F;YAC1F,kCAAkC;QACpC,WAAW,EAAE;YACX,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE;gBACV,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBAC1B,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE;gBACrD,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,sCAAsC,EAAE;aAC/E;YACD,QAAQ,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC;SAC9B;KACF;IACD;QACE,IAAI,EAAE,wBAAwB;QAC9B,WAAW,EACT,qFAAqF;YACrF,0FAA0F;YAC1F,2CAA2C;QAC7C,WAAW,EAAE;YACX,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;YAC1C,QAAQ,EAAE,CAAC,QAAQ,CAAC;SACrB;KACF;IACD;QACE,IAAI,EAAE,uBAAuB;QAC7B,WAAW,EACT,iFAAiF;YACjF,qFAAqF;QACvF,WAAW,EAAE;YACX,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;YAC1C,QAAQ,EAAE,CAAC,QAAQ,CAAC;SACrB;KACF;IACD;QACE,IAAI,EAAE,cAAc;QACpB,WAAW,EACT,sFAAsF;YACtF,oEAAoE;QACtE,WAAW,EAAE;YACX,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE;gBACV,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBAC1B,GAAG,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE;gBACnE,OAAO,EAAE,aAAa;gBACtB,MAAM,EAAE;oBACN,IAAI,EAAE,OAAO;oBACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;oBACzB,WAAW,EAAE,uDAAuD;iBACrE;gBACD,WAAW,EAAE;oBACX,IAAI,EAAE,QAAQ;oBACd,WAAW,EAAE,6DAA6D;iBAC3E;gBACD,yBAAyB,EAAE;oBACzB,IAAI,EAAE,SAAS;oBACf,WAAW,EACT,yEAAyE;wBACzE,8DAA8D;iBACjE;aACF;YACD,QAAQ,EAAE,CAAC,QAAQ,CAAC;SACrB;KACF;IACD;QACE,IAAI,EAAE,oBAAoB;QAC1B,WAAW,EAAE,yEAAyE;QACtF,WAAW,EAAE;YACX,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE;gBACV,MAAM,EAAE;oBACN,IAAI,EAAE,QAAQ;oBACd,IAAI,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC;oBACvB,WAAW,EAAE,+CAA+C;iBAC7D;gBACD,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,0CAA0C,EAAE;aACpF;SACF;KACF;IACD;QACE,IAAI,EAAE,gBAAgB;QACtB,WAAW,EACT,kFAAkF;YAClF,oDAAoD;QACtD,WAAW,EAAE;YACX,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE;gBACV,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,sBAAsB,EAAE;gBAChE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,+CAA+C,EAAE;gBACxF,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,4CAA4C,EAAE;gBACnF,QAAQ,EAAE;oBACR,IAAI,EAAE,SAAS;oBACf,WAAW,EAAE,mEAAmE;iBACjF;aACF;SACF;KACF;IACD;QACE,IAAI,EAAE,qBAAqB;QAC3B,WAAW,EACT,gFAAgF;YAChF,qFAAqF;YACrF,6BAA6B;QAC/B,WAAW,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE;KAChD;IACD;QACE,IAAI,EAAE,yBAAyB;QAC/B,WAAW,EACT,iFAAiF;YACjF,gFAAgF;YAChF,2CAA2C;QAC7C,WAAW,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE;KAChD;CACF,CAAC;AAIF,SAAS,QAAQ,CAAC,IAAU,EAAE,GAAW,EAAE,QAAQ,GAAG,KAAK;IACzD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;IACtB,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QACtC,IAAI,QAAQ;YAAE,MAAM,IAAI,WAAW,CAAC,8BAA8B,GAAG,EAAE,CAAC,CAAC;QACzE,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,MAAM,IAAI,WAAW,CAAC,aAAa,GAAG,oBAAoB,CAAC,CAAC;IACzF,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,QAAQ,CAAC,IAAU,EAAE,GAAW,EAAE,QAAgB;IACzD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;IACtB,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,QAAQ,CAAC;IACvD,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IACtB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;QAAE,MAAM,IAAI,WAAW,CAAC,aAAa,GAAG,oBAAoB,CAAC,CAAC;IACrF,OAAO,CAAC,CAAC;AACX,CAAC;AAED,SAAS,SAAS,CAAC,IAAU,EAAE,GAAW,EAAE,QAAiB;IAC3D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;IACtB,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,QAAQ,CAAC;IACvD,IAAI,OAAO,GAAG,KAAK,SAAS;QAAE,OAAO,GAAG,CAAC;IACzC,IAAI,GAAG,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IAChC,IAAI,GAAG,KAAK,OAAO;QAAE,OAAO,KAAK,CAAC;IAClC,MAAM,IAAI,WAAW,CAAC,aAAa,GAAG,qBAAqB,CAAC,CAAC;AAC/D,CAAC;AAED,SAAS,SAAS,CAAC,IAAU;IAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC;IAC5B,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IACjD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;QAAE,MAAM,IAAI,WAAW,CAAC,qCAAqC,CAAC,CAAC;IACtF,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QAC7B,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACrE,MAAM,IAAI,WAAW,CAAC,WAAW,KAAK,qBAAqB,CAAC,CAAC;QAC/D,CAAC;QACD,OAAO,IAA+B,CAAC;IACzC,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,aAAa,CAAC,IAAU,EAAE,GAAW;IAC5C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;IACtB,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IACxD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;QAAE,MAAM,IAAI,WAAW,CAAC,aAAa,GAAG,oBAAoB,CAAC,CAAC;IACrF,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACtB,IAAI,OAAO,CAAC,KAAK,QAAQ;YAAE,MAAM,IAAI,WAAW,CAAC,GAAG,GAAG,IAAI,CAAC,oBAAoB,CAAC,CAAC;QAClF,OAAO,CAAC,CAAC;IACX,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,WAAY,SAAQ,KAAK;CAAG;AAElC;;;;;;GAMG;AACH,KAAK,UAAU,QAAQ,CAAC,IAAY,EAAE,IAAU;IAC9C,IAAI,IAAI,KAAK,yBAAyB,EAAE,CAAC;QACvC,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC;QACxC,MAAM,IAAI,GAAG,MAAM,WAAW,EAAE,CAAC;QACjC,OAAO;YACL,GAAG,MAAM;YACT,cAAc,EAAE,IAAI,CAAC,EAAE;gBACrB,CAAC,CAAC,IAAI,CAAC,IAAI;gBACX,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE;SAC5F,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,aAAa,EAAE,CAAC;IACpC,IAAI,CAAC,KAAK,CAAC,EAAE;QAAE,MAAM,IAAI,cAAc,CAAC,KAAK,CAAC,CAAC;IAE/C,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,cAAc,CAAC,CAAC,CAAC;YACpB,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,CAAE,CAAC;YACjD,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE;gBAAE,MAAM,IAAI,WAAW,CAAC,2BAA2B,CAAC,CAAC;YACxE,OAAO,IAAI,CAAC;gBACV,QAAQ,EAAE,SAAS;gBACnB,MAAM,EAAE;oBACN,OAAO;oBACP,KAAK,EAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,CAAC;oBAClC,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;oBACnC,OAAO,EAAE,SAAS,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,CAAC;iBAC3C;gBACD,SAAS,EAAE,iBAAiB;aAC7B,CAAC,CAAC;QACL,CAAC;QACD,KAAK,cAAc;YACjB,OAAO,IAAI,CAAC;gBACV,QAAQ,EAAE,SAAS;gBACnB,MAAM,EAAE;oBACN,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAE;oBACvC,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC;oBACxB,OAAO,EAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,IAAI;oBAC1C,UAAU,EAAE,SAAS,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,CAAC;oBAChD,KAAK,EAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,CAAC;oBAClC,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;iBACpC;aACF,CAAC,CAAC;QACL,KAAK,mBAAmB;YACtB,OAAO,IAAI,CAAC;gBACV,QAAQ,EAAE,cAAc;gBACxB,MAAM,EAAE;oBACN,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAE;oBACvC,KAAK,EAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAE;oBACrC,KAAK,EAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,CAAC;iBACnC;aACF,CAAC,CAAC;QACL,KAAK,wBAAwB;YAC3B,OAAO,IAAI,CAAC;gBACV,QAAQ,EAAE,mBAAmB;gBAC7B,MAAM,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAE,EAAE;aACpD,CAAC,CAAC;QACL,KAAK,uBAAuB;YAC1B,OAAO,IAAI,CAAC;gBACV,QAAQ,EAAE,kBAAkB;gBAC5B,MAAM,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAE,EAAE;aACpD,CAAC,CAAC;QACL,KAAK,cAAc;YACjB,OAAO,IAAI,CAAC;gBACV,QAAQ,EAAE,SAAS;gBACnB,MAAM,EAAE;oBACN,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAE;oBACvC,GAAG,EAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,KAAK;oBACnC,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC;oBACxB,MAAM,EAAE,aAAa,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,IAAI;oBAC7C,yBAAyB,EAAE,SAAS,CAAC,IAAI,EAAE,2BAA2B,EAAE,IAAI,CAAC;iBAC9E;aACF,CAAC,CAAC;QACL,KAAK,oBAAoB;YACvB,OAAO,IAAI,CAAC;gBACV,QAAQ,EAAE,eAAe;gBACzB,MAAM,EAAE;oBACN,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,MAAM;oBAC1C,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,IAAI;iBACzC;aACF,CAAC,CAAC;QACL,KAAK,gBAAgB;YACnB,OAAO,IAAI,CAAC;gBACV,QAAQ,EAAE,WAAW;gBACrB,MAAM,EAAE;oBACN,OAAO,EAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,IAAI;oBAC1C,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,IAAI;oBACxC,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI;oBACpC,QAAQ,EAAE,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,CAAC;iBAC5C;gBACD,SAAS,EAAE,iBAAiB;aAC7B,CAAC,CAAC;QACL,KAAK,qBAAqB;YACxB,OAAO,IAAI,CAAC,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC;QAC1D;YACE,MAAM,IAAI,WAAW,CAAC,iBAAiB,IAAI,EAAE,CAAC,CAAC;IACnD,CAAC;AACH,CAAC;AAED,MAAM,cAAe,SAAQ,KAAK;IACX;IAArB,YAAqB,OAAmB;QACtC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QADJ,YAAO,GAAP,OAAO,CAAY;IAExC,CAAC;CACF;AAED;;;;;;GAMG;AACH,KAAK,UAAU,IAAI,CAAC,IAInB;IACC,MAAM,QAAQ,GAA2B,EAAE,CAAC;IAC5C,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS;QAAE,QAAQ,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;IAEtE,MAAM,MAAM,GAAG,MAAM,WAAW,CAA0B,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAChG,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,MAAM,IAAI,cAAc,CAAC,MAAM,CAAC,CAAC;IAEjD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;IACzB,4EAA4E;IAC5E,yCAAyC;IACzC,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,uBAAuB,CAAC,KAAK,qBAAqB,CAAC;QACxD,IAAI,CAAC,oBAAoB,CAAC,KAAK,kBAAkB,CAAC;IACpD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,UAAU,CAAC,KAAc;IAChC,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;KAC3E,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,IAAY,EAAE,OAAe,EAAE,IAAa;IAC/D,MAAM,OAAO,GAA4B,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IACxF,IAAI,IAAI;QAAE,OAAO,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IACjC,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;QAC5E,OAAO,EAAE,IAAI;KACd,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,oBAAoB,EAAE,CAAC;IAEvB,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,EAAE,EAC9C,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAChC,CAAC;IAEF,MAAM,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IAEjF,MAAM,CAAC,iBAAiB,CAAC,qBAAqB,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QAChE,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC;QACjC,MAAM,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,CAAS,CAAC;QAEtD,IAAI,CAAC;YACH,OAAO,UAAU,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;QAChD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,GAAG,YAAY,WAAW,EAAE,CAAC;gBAC/B,OAAO,WAAW,CAAC,kBAAkB,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;YACtD,CAAC;YACD,IAAI,GAAG,YAAY,cAAc,EAAE,CAAC;gBAClC,OAAO,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAC9E,CAAC;YACD,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACjE,sEAAsE;YACtE,iEAAiE;YACjE,OAAO,WAAW,CAChB,gBAAgB,EAChB,OAAO,EACP,2DAA2D,CAC5D,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAEhC,wEAAwE;IACxE,+CAA+C;IAC/C,KAAK,YAAY,EAAE;SAChB,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;QACf,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,WAAW,KAAK,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC;QAC7D,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YAClB,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,OAAO;gBAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,MAAM,IAAI,CAAC,CAAC;QAC/E,CAAC;IACH,CAAC,CAAC;SACD,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;AACrB,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,IAAI,WAAW,YAAY,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAC/F,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,OAAO,EAAE,WAAW,EAAE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ictrp-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "MCP server for the WHO International Clinical Trials Registry Platform (ICTRP). Plain HTTP, no browser. Every response is explicit about the export's known incompleteness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
package/sidecar/ictrp_sidecar.py
CHANGED
|
@@ -85,19 +85,21 @@ from ictrp_mcp.data import query as query_mod # noqa: E402
|
|
|
85
85
|
from ictrp_mcp.errors import ErrorCode, IctrpError # noqa: E402
|
|
86
86
|
from ictrp_mcp.cache.store import SetStore, default_cache_dir, normalize_query # noqa: E402
|
|
87
87
|
from ictrp_mcp.ictrp.session import run_chain # noqa: E402
|
|
88
|
-
from ictrp_mcp.provenance import
|
|
88
|
+
from ictrp_mcp.provenance import ( # noqa: E402
|
|
89
|
+
INCOMPLETENESS_NOTICE,
|
|
90
|
+
SHORTFALL_NOTICE,
|
|
91
|
+
Provenance,
|
|
92
|
+
classify_completeness,
|
|
93
|
+
)
|
|
89
94
|
from ictrp_mcp.tools import DEFAULT_FIELDS, IctrpService # noqa: E402
|
|
90
95
|
|
|
91
96
|
LOG = logging.getLogger("ictrp-sidecar")
|
|
92
97
|
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"records the portal itself reports as matches, so absence is not evidence "
|
|
99
|
-
"of nonexistence."
|
|
100
|
-
)
|
|
98
|
+
# The unit notice is defined once in the package and repeated verbatim on every
|
|
99
|
+
# row-bearing response here. The Node layer also attaches it, but the sidecar
|
|
100
|
+
# must not depend on the caller remembering: anything that reads this HTTP API
|
|
101
|
+
# directly deserves the same wording.
|
|
102
|
+
_INCOMPLETENESS_NOTICE = INCOMPLETENESS_NOTICE
|
|
101
103
|
|
|
102
104
|
DEFAULT_HOST = "127.0.0.1"
|
|
103
105
|
DEFAULT_PORT = 8849
|
|
@@ -365,17 +367,45 @@ def op_offline_lookup(keyword: str) -> dict[str, Any]:
|
|
|
365
367
|
}
|
|
366
368
|
snapshot, path = loaded
|
|
367
369
|
trials = [t.to_dict() if hasattr(t, "to_dict") else t for t in snapshot.trials]
|
|
368
|
-
|
|
370
|
+
# Counts come from the snapshot's own provenance, never from a fixed guess.
|
|
371
|
+
# A bundle written by 0.1.2 carries only the old record figure, and a bundle
|
|
372
|
+
# may have been built from a search whose portal page did not state a trial
|
|
373
|
+
# count at all -- both yield `unknown`, which is honest. Hardcoding "incomplete"
|
|
374
|
+
# here is what made every offline answer claim a shortfall that did not exist.
|
|
375
|
+
snapshot_prov = Provenance.from_dict(snapshot.provenance or {})
|
|
376
|
+
rows = len(trials)
|
|
377
|
+
completeness, short = classify_completeness(
|
|
378
|
+
rows, snapshot_prov.upstream_reported_trials
|
|
379
|
+
)
|
|
380
|
+
payload: dict[str, Any] = {
|
|
369
381
|
"ok": True,
|
|
370
382
|
"found": True,
|
|
371
383
|
"keyword": keyword,
|
|
372
384
|
"path": str(path),
|
|
373
|
-
"rows_returned":
|
|
374
|
-
"
|
|
385
|
+
"rows_returned": rows,
|
|
386
|
+
"completeness": completeness,
|
|
375
387
|
"incompleteness_notice": _INCOMPLETENESS_NOTICE,
|
|
376
|
-
"provenance": offline.snapshot_provenance(snapshot, path),
|
|
388
|
+
"provenance": {**(offline.snapshot_provenance(snapshot, path)),
|
|
389
|
+
"completeness": completeness},
|
|
377
390
|
"trials": trials,
|
|
378
391
|
}
|
|
392
|
+
if snapshot_prov.upstream_reported_records is not None:
|
|
393
|
+
payload["upstream_reported_records"] = snapshot_prov.upstream_reported_records
|
|
394
|
+
if snapshot_prov.upstream_reported_trials is not None:
|
|
395
|
+
payload["upstream_reported_trials"] = snapshot_prov.upstream_reported_trials
|
|
396
|
+
if short is not None:
|
|
397
|
+
payload["rows_short_of_trials"] = short
|
|
398
|
+
payload["shortfall_notice"] = SHORTFALL_NOTICE
|
|
399
|
+
if (
|
|
400
|
+
snapshot_prov.upstream_reported_records is not None
|
|
401
|
+
and snapshot_prov.upstream_reported_trials is not None
|
|
402
|
+
):
|
|
403
|
+
payload["duplicate_records"] = max(
|
|
404
|
+
0,
|
|
405
|
+
snapshot_prov.upstream_reported_records
|
|
406
|
+
- snapshot_prov.upstream_reported_trials,
|
|
407
|
+
)
|
|
408
|
+
return payload
|
|
379
409
|
|
|
380
410
|
|
|
381
411
|
# --------------------------------------------------------------------------
|
|
@@ -28,6 +28,7 @@ from typing import Any
|
|
|
28
28
|
|
|
29
29
|
from ..errors import ErrorCode, IctrpError
|
|
30
30
|
from ..data.normalize import to_trial
|
|
31
|
+
from ..provenance import classify_completeness
|
|
31
32
|
|
|
32
33
|
#: Parsed sets held in memory at once.
|
|
33
34
|
MAX_MATERIALIZED_SETS = 3
|
|
@@ -68,32 +69,47 @@ class MaterializedSet:
|
|
|
68
69
|
keyword: str
|
|
69
70
|
trials: list[dict[str, Any]]
|
|
70
71
|
header: tuple[str, ...]
|
|
71
|
-
|
|
72
|
+
reported_records: int | None
|
|
73
|
+
reported_trials: int | None
|
|
72
74
|
csv_bytes: int
|
|
73
75
|
created_at: float
|
|
74
76
|
provenance: dict[str, Any] = field(default_factory=dict)
|
|
75
77
|
|
|
76
78
|
@property
|
|
77
79
|
def rows_returned(self) -> int:
|
|
78
|
-
"""How many rows we actually hold.
|
|
80
|
+
"""How many rows we actually hold -- one per TRIAL.
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
Comparable to the portal's TRIAL count, never to its RECORD count: one
|
|
83
|
+
trial registered in several registries is several records and one row
|
|
84
|
+
here. See docs/VERIFICATION_29PCT.md.
|
|
82
85
|
"""
|
|
83
86
|
return len(self.trials)
|
|
84
87
|
|
|
85
88
|
@property
|
|
86
|
-
def
|
|
87
|
-
"""
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
def completeness(self) -> str:
|
|
90
|
+
"""`trials_complete`, `trials_short`, or `unknown`.
|
|
91
|
+
|
|
92
|
+
`trials_short` is the real problem case: the export holds fewer rows
|
|
93
|
+
than the portal says there are trials.
|
|
94
|
+
"""
|
|
95
|
+
completeness, _ = classify_completeness(self.rows_returned, self.reported_trials)
|
|
96
|
+
return completeness
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def rows_short_of_trials(self) -> int | None:
|
|
100
|
+
"""Trials the portal lists that this export does not hold, if knowable."""
|
|
101
|
+
_, short = classify_completeness(self.rows_returned, self.reported_trials)
|
|
102
|
+
return short
|
|
91
103
|
|
|
92
104
|
@property
|
|
93
|
-
def
|
|
94
|
-
|
|
105
|
+
def duplicate_records(self) -> int | None:
|
|
106
|
+
"""Records the portal counted more than once, when it reported both units.
|
|
107
|
+
|
|
108
|
+
This is the number that used to be presented as "missing trials".
|
|
109
|
+
"""
|
|
110
|
+
if self.reported_records is None or self.reported_trials is None:
|
|
95
111
|
return None
|
|
96
|
-
return max(0, self.
|
|
112
|
+
return max(0, self.reported_records - self.reported_trials)
|
|
97
113
|
|
|
98
114
|
def age_seconds(self) -> float:
|
|
99
115
|
return time.time() - self.created_at
|
|
@@ -103,9 +119,11 @@ class MaterializedSet:
|
|
|
103
119
|
"set_id": self.set_id,
|
|
104
120
|
"keyword": self.keyword,
|
|
105
121
|
"rows_returned": self.rows_returned,
|
|
106
|
-
"
|
|
107
|
-
"
|
|
108
|
-
"
|
|
122
|
+
"upstream_reported_records": self.reported_records,
|
|
123
|
+
"upstream_reported_trials": self.reported_trials,
|
|
124
|
+
"completeness": self.completeness,
|
|
125
|
+
"rows_short_of_trials": self.rows_short_of_trials,
|
|
126
|
+
"duplicate_records": self.duplicate_records,
|
|
109
127
|
"csv_bytes": self.csv_bytes,
|
|
110
128
|
"age_seconds": round(self.age_seconds(), 1),
|
|
111
129
|
"created_at": _iso(self.created_at),
|
|
@@ -148,7 +166,8 @@ class SetStore:
|
|
|
148
166
|
payload_raw: bytes,
|
|
149
167
|
header: tuple[str, ...],
|
|
150
168
|
rows: list[list[str]],
|
|
151
|
-
|
|
169
|
+
reported_records: int | None,
|
|
170
|
+
reported_trials: int | None,
|
|
152
171
|
provenance: dict[str, Any],
|
|
153
172
|
) -> MaterializedSet:
|
|
154
173
|
"""Persist and materialize a validated export."""
|
|
@@ -159,7 +178,8 @@ class SetStore:
|
|
|
159
178
|
keyword=keyword,
|
|
160
179
|
trials=trials,
|
|
161
180
|
header=header,
|
|
162
|
-
|
|
181
|
+
reported_records=reported_records,
|
|
182
|
+
reported_trials=reported_trials,
|
|
163
183
|
csv_bytes=len(payload_raw),
|
|
164
184
|
created_at=time.time(),
|
|
165
185
|
provenance=dict(provenance),
|
|
@@ -173,7 +193,13 @@ class SetStore:
|
|
|
173
193
|
{
|
|
174
194
|
"keyword": keyword,
|
|
175
195
|
"header": list(header),
|
|
176
|
-
"
|
|
196
|
+
"reported_records": reported_records,
|
|
197
|
+
"reported_trials": reported_trials,
|
|
198
|
+
# Written by 0.1.2, where this slot held the portal's
|
|
199
|
+
# RECORD count under a name that implied trials. Kept so
|
|
200
|
+
# an older reader still finds a key, never read back --
|
|
201
|
+
# reading it would resurrect the false shortfall.
|
|
202
|
+
"reported_total": None,
|
|
177
203
|
"created_at": materialized.created_at,
|
|
178
204
|
"csv_bytes": materialized.csv_bytes,
|
|
179
205
|
"provenance": provenance,
|
|
@@ -206,13 +232,18 @@ class SetStore:
|
|
|
206
232
|
The set is memory-resident only. Writing it back as a raw CSV would
|
|
207
233
|
fabricate an export we never received, and `_reload_from_disk` would then
|
|
208
234
|
present it as a genuine export on the next run.
|
|
235
|
+
|
|
236
|
+
Counts come from the snapshot's own provenance, and may legitimately be
|
|
237
|
+
absent: a snapshot taken before the portal's trial count became readable
|
|
238
|
+
yields `completeness = unknown` rather than an invented shortfall.
|
|
209
239
|
"""
|
|
210
240
|
materialized = MaterializedSet(
|
|
211
241
|
set_id=query_key(keyword),
|
|
212
242
|
keyword=keyword,
|
|
213
243
|
trials=list(trials),
|
|
214
244
|
header=(),
|
|
215
|
-
|
|
245
|
+
reported_records=provenance.get("upstream_reported_records"),
|
|
246
|
+
reported_trials=provenance.get("upstream_reported_trials"),
|
|
216
247
|
csv_bytes=0,
|
|
217
248
|
created_at=created_at if created_at is not None else time.time(),
|
|
218
249
|
provenance=dict(provenance),
|
|
@@ -277,7 +308,8 @@ class SetStore:
|
|
|
277
308
|
keyword=meta.get("keyword", ""),
|
|
278
309
|
trials=[to_trial(r, header) for r in rows],
|
|
279
310
|
header=header,
|
|
280
|
-
|
|
311
|
+
reported_records=meta.get("reported_records"),
|
|
312
|
+
reported_trials=meta.get("reported_trials"),
|
|
281
313
|
csv_bytes=meta.get("csv_bytes", len(raw)),
|
|
282
314
|
created_at=created_at,
|
|
283
315
|
provenance=meta.get("provenance", {}) or {},
|
|
@@ -323,6 +323,18 @@ def to_trial(row: list[str], header: tuple[str, ...]) -> dict[str, Any]:
|
|
|
323
323
|
trial["inclusion_age_min"] = _as_int(age_min)
|
|
324
324
|
trial["inclusion_age_max"] = _as_int(age_max)
|
|
325
325
|
|
|
326
|
+
# ICTRP has no structured "line of therapy" column. Treatment line is stated
|
|
327
|
+
# only inside the free-text inclusion/exclusion criteria (e.g. "first-line",
|
|
328
|
+
# "previously treated", "一线") and, frequently, the scientific title
|
|
329
|
+
# ("...Received >=2 Prior Lines of Therapy"). Derive a non-authoritative hint
|
|
330
|
+
# from that text so a caller can surface it without re-parsing. It is a hint,
|
|
331
|
+
# never a fact: absence here does NOT mean the trial is treatment-naive.
|
|
332
|
+
trial["line_of_therapy_hint"] = derive_line_of_therapy_hint(
|
|
333
|
+
raw.get("inclusion_criteria"),
|
|
334
|
+
raw.get("exclusion_criteria"),
|
|
335
|
+
raw.get("scientific_title"),
|
|
336
|
+
)
|
|
337
|
+
|
|
326
338
|
# `results yes no` is a flag, not content, so it is excluded from the test.
|
|
327
339
|
# Measured: the substantive `results *` columns are ~0.0% populated for
|
|
328
340
|
# ChiCTR records, so this is a property of the registry rather than of an
|
|
@@ -346,3 +358,66 @@ def _as_int(value: str | None) -> int | None:
|
|
|
346
358
|
return None
|
|
347
359
|
m = re.search(r"-?\d+", text)
|
|
348
360
|
return int(m.group(0)) if m else None
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
#: Treatment-line vocabulary, as it actually appears in live inclusion/exclusion
|
|
364
|
+
#: text. Both English and Chinese markers are matched; "refractory"/"relapsed"/
|
|
365
|
+
#: "previously treated" indicate a later line without naming a number.
|
|
366
|
+
_LINE_PATTERNS: tuple[tuple[str, str], ...] = (
|
|
367
|
+
(r"first[-\s]?line|1st[-\s]?line|一线|treatment[-\s]?naive|previously untreated", "first-line"),
|
|
368
|
+
(r"second[-\s]?line|2nd[-\s]?line|二线", "second-line"),
|
|
369
|
+
(r"third[-\s]?line|3rd[-\s]?line|三线", "third-line"),
|
|
370
|
+
(r"fourth[-\s]?line|4th[-\s]?line|四线", "fourth-line"),
|
|
371
|
+
(r"relapsed|refractory|previously treated|prior (therapy|treatment)|已接受.*治疗|经治", "later-line"),
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
#: Numeric "line" statements, e.g. ">=2 Prior Lines of Therapy" or "2-line",
|
|
375
|
+
#: where a word may sit between the number and "line" ("2 prior lines"). The
|
|
376
|
+
#: captured number is the *minimum* line; ">=2" means second-line or later, never
|
|
377
|
+
#: first-line, so it resolves to `later-line` rather than a specific line.
|
|
378
|
+
_LINE_NUMBER = re.compile(r"(\d+)\s*(?:[-\w.,]+\s)*?(?:line|lines|线)")
|
|
379
|
+
|
|
380
|
+
_WORD_NUMBERS = {
|
|
381
|
+
"one": 1, "two": 2, "three": 3, "four": 4, "five": 5,
|
|
382
|
+
"一": 1, "二": 2, "三": 3, "四": 4, "五": 5,
|
|
383
|
+
}
|
|
384
|
+
_WORD_NUMBER = re.compile(r"(one|two|three|four|five|一|二|三|四|五)\s*(?:line|lines|线)")
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def derive_line_of_therapy_hint(
|
|
388
|
+
inclusion: str | None, exclusion: str | None, scientific_title: str | None = None
|
|
389
|
+
) -> str | None:
|
|
390
|
+
"""Best-effort treatment-line label from free-text criteria.
|
|
391
|
+
|
|
392
|
+
Returns the earliest explicit line if one is named (first > second > third >
|
|
393
|
+
fourth), else "later-line" when a numeric "N lines" (N>=2) or relapse/
|
|
394
|
+
refractory language is present, else `None`. `None` is deliberate: it means
|
|
395
|
+
"not stated in the criteria we hold", which must not be read as "first-line"
|
|
396
|
+
-- the export is incomplete and the column does not exist.
|
|
397
|
+
|
|
398
|
+
The scientific title is included because the line is often stated only there
|
|
399
|
+
(e.g. "...Received >=2 Prior Lines of Therapy"), not in the criteria body.
|
|
400
|
+
"""
|
|
401
|
+
text = f"{inclusion or ''} {exclusion or ''} {scientific_title or ''}"
|
|
402
|
+
if not text.strip():
|
|
403
|
+
return None
|
|
404
|
+
from .query import _coerce # local import avoids a cycle at module load
|
|
405
|
+
|
|
406
|
+
lowered = _coerce(text)
|
|
407
|
+
|
|
408
|
+
# Explicit worded lines take precedence -- they name a specific line.
|
|
409
|
+
for pattern, label in _LINE_PATTERNS:
|
|
410
|
+
if re.search(pattern, lowered):
|
|
411
|
+
return label
|
|
412
|
+
|
|
413
|
+
# Numeric "N line(s)": a minimum of N, so only the first-line case is exact;
|
|
414
|
+
# anything >=2 is "later-line", never a specific later number.
|
|
415
|
+
for match in _LINE_NUMBER.finditer(lowered):
|
|
416
|
+
if int(match.group(1)) <= 1:
|
|
417
|
+
return "first-line"
|
|
418
|
+
return "later-line"
|
|
419
|
+
for match in _WORD_NUMBER.finditer(lowered):
|
|
420
|
+
if _WORD_NUMBERS[match.group(1)] <= 1:
|
|
421
|
+
return "first-line"
|
|
422
|
+
return "later-line"
|
|
423
|
+
return None
|
|
@@ -16,6 +16,7 @@ import csv
|
|
|
16
16
|
import io
|
|
17
17
|
import re
|
|
18
18
|
from dataclasses import dataclass
|
|
19
|
+
from typing import NamedTuple
|
|
19
20
|
|
|
20
21
|
from ..data.columns import REQUIRED_COLUMNS
|
|
21
22
|
from ..errors import ErrorCode, IctrpError
|
|
@@ -54,7 +55,8 @@ class CsvPayload:
|
|
|
54
55
|
header: tuple[str, ...]
|
|
55
56
|
rows: list[list[str]]
|
|
56
57
|
content_type: str | None
|
|
57
|
-
|
|
58
|
+
reported_records: int | None = None
|
|
59
|
+
reported_trials: int | None = None
|
|
58
60
|
|
|
59
61
|
@property
|
|
60
62
|
def row_count(self) -> int:
|
|
@@ -66,6 +68,28 @@ class CsvPayload:
|
|
|
66
68
|
return len(self.rows) == 0
|
|
67
69
|
|
|
68
70
|
|
|
71
|
+
class ReportedCounts(NamedTuple):
|
|
72
|
+
"""The portal's own two figures, in the two units it actually uses.
|
|
73
|
+
|
|
74
|
+
The results page prints one sentence carrying both, e.g. for KRAS::
|
|
75
|
+
|
|
76
|
+
1243 records for 882 trials found for: KRAS
|
|
77
|
+
|
|
78
|
+
``records`` counts registry ROWS -- a trial registered in several registries
|
|
79
|
+
is one trial but several records. ``trials`` counts TRIALS after the portal
|
|
80
|
+
has grouped records that reference each other via Secondary ID. An export
|
|
81
|
+
carries one row per trial, so its row count is comparable to ``trials`` and
|
|
82
|
+
NOT to ``records``. Comparing the two is what produced the false "CSV is
|
|
83
|
+
short by up to 29%" warning documented in docs/VERIFICATION_29PCT.md.
|
|
84
|
+
|
|
85
|
+
Both fields are Optional and independent: a partial sentence yields None for
|
|
86
|
+
the missing unit rather than a guess. Never fall back to one for the other.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
records: int | None
|
|
90
|
+
trials: int | None
|
|
91
|
+
|
|
92
|
+
|
|
69
93
|
def _excerpt(body: bytes | str, limit: int = _MAX_EXCERPT) -> str:
|
|
70
94
|
if isinstance(body, bytes):
|
|
71
95
|
text = body.decode("utf-8", "replace")
|
|
@@ -132,7 +156,8 @@ def classify_export(
|
|
|
132
156
|
content_disposition: str | None,
|
|
133
157
|
body: bytes,
|
|
134
158
|
location: str | None = None,
|
|
135
|
-
|
|
159
|
+
reported_records: int | None = None,
|
|
160
|
+
reported_trials: int | None = None,
|
|
136
161
|
) -> CsvPayload:
|
|
137
162
|
"""Classify an export response.
|
|
138
163
|
|
|
@@ -212,7 +237,8 @@ def classify_export(
|
|
|
212
237
|
header=header,
|
|
213
238
|
rows=rows,
|
|
214
239
|
content_type=content_type,
|
|
215
|
-
|
|
240
|
+
reported_records=reported_records,
|
|
241
|
+
reported_trials=reported_trials,
|
|
216
242
|
)
|
|
217
243
|
|
|
218
244
|
|
|
@@ -250,20 +276,66 @@ def classify_form_page(*, status: int, body: bytes) -> str:
|
|
|
250
276
|
return text
|
|
251
277
|
|
|
252
278
|
|
|
253
|
-
|
|
279
|
+
#: The sentence the portal prints above the results table, carrying BOTH of its
|
|
280
|
+
#: counts. Order and wording are anchored, and the unit words are mandatory --
|
|
281
|
+
#: the whole point of this change is that "1243 records" must never be read as a
|
|
282
|
+
#: trial count just because the other half of the sentence was not matched.
|
|
283
|
+
_ACCOUNTING_RE = re.compile(
|
|
284
|
+
r"([\d][\d,\s]*?)\s*records?\s+for\s+([\d][\d,\s]*?)\s*trials?\s+found\s+for",
|
|
285
|
+
re.I,
|
|
286
|
+
)
|
|
254
287
|
|
|
288
|
+
#: Record count alone. Used only to report the unit we could not resolve, never
|
|
289
|
+
#: as a substitute for a trial count.
|
|
290
|
+
_RECORDS_RE = re.compile(r"([\d][\d,\s]*?)\s*records\b", re.I)
|
|
255
291
|
|
|
256
|
-
|
|
257
|
-
|
|
292
|
+
#: Trial count alone, for a portal that stops printing the record half.
|
|
293
|
+
_TRIALS_RE = re.compile(r"([\d][\d,\s]*?)\s*trials?\b", re.I)
|
|
258
294
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
surface both and never conflate them. See docs/MEASUREMENTS.md section 2.
|
|
262
|
-
"""
|
|
263
|
-
match = _RECORDS_RE.search(html)
|
|
264
|
-
if not match:
|
|
265
|
-
return None
|
|
295
|
+
|
|
296
|
+
def _to_int(raw: str) -> int | None:
|
|
266
297
|
try:
|
|
267
|
-
return int(
|
|
298
|
+
return int(re.sub(r"[,\s]", "", raw))
|
|
268
299
|
except ValueError:
|
|
269
300
|
return None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def parse_reported_counts(html: str) -> ReportedCounts | None:
|
|
304
|
+
"""Extract the portal's own 'N records for M trials' figures from a results page.
|
|
305
|
+
|
|
306
|
+
Returns None when the page carries neither unit. When only one half of the
|
|
307
|
+
sentence is present, the other half is None -- deliberately NOT copied from
|
|
308
|
+
the half that was found, because that substitution is the bug this function
|
|
309
|
+
exists to remove (see docs/VERIFICATION_29PCT.md).
|
|
310
|
+
|
|
311
|
+
These are the portal's claims, not verified counts of what we received.
|
|
312
|
+
"""
|
|
313
|
+
text = html.decode("utf-8", "replace") if isinstance(html, bytes) else html
|
|
314
|
+
|
|
315
|
+
match = _ACCOUNTING_RE.search(text)
|
|
316
|
+
if match:
|
|
317
|
+
records = _to_int(match.group(1))
|
|
318
|
+
trials = _to_int(match.group(2))
|
|
319
|
+
if records is not None and trials is not None:
|
|
320
|
+
return ReportedCounts(records=records, trials=trials)
|
|
321
|
+
|
|
322
|
+
records_match = _RECORDS_RE.search(text)
|
|
323
|
+
trials_match = _TRIALS_RE.search(text)
|
|
324
|
+
records = _to_int(records_match.group(1)) if records_match else None
|
|
325
|
+
trials = _to_int(trials_match.group(1)) if trials_match else None
|
|
326
|
+
if records is None and trials is None:
|
|
327
|
+
return None
|
|
328
|
+
return ReportedCounts(records=records, trials=trials)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def parse_reported_total(html: str) -> int | None:
|
|
332
|
+
"""Deprecated. Use `parse_reported_counts`.
|
|
333
|
+
|
|
334
|
+
Kept for one minor release because it is a public name. WARNING: the value
|
|
335
|
+
it returns is the portal's RECORD count (registry rows), not its TRIAL
|
|
336
|
+
count. Subtracting an export's row count from it measures cross-registry
|
|
337
|
+
duplicates, not missing trials -- that mistake shipped as a false warning in
|
|
338
|
+
every response from 0.1.2. Will be removed in the next minor release.
|
|
339
|
+
"""
|
|
340
|
+
counts = parse_reported_counts(html)
|
|
341
|
+
return None if counts is None else counts.records
|
|
@@ -31,9 +31,10 @@ from ..errors import ErrorCode, IctrpError
|
|
|
31
31
|
from . import htmlstate
|
|
32
32
|
from .export_guard import (
|
|
33
33
|
CsvPayload,
|
|
34
|
+
ReportedCounts,
|
|
34
35
|
classify_export,
|
|
35
36
|
classify_form_page,
|
|
36
|
-
|
|
37
|
+
parse_reported_counts,
|
|
37
38
|
)
|
|
38
39
|
|
|
39
40
|
BASE_URL = "https://trialsearch.who.int/Default.aspx"
|
|
@@ -224,7 +225,8 @@ class IctrpSession:
|
|
|
224
225
|
last_html: str = ""
|
|
225
226
|
form_state: htmlstate.FormState | None = None
|
|
226
227
|
results_state: htmlstate.FormState | None = None
|
|
227
|
-
|
|
228
|
+
#: The portal's own counts, in its own two units. See `ReportedCounts`.
|
|
229
|
+
reported_counts: ReportedCounts | None = None
|
|
228
230
|
response_date: str | None = None
|
|
229
231
|
steps: list[str] = field(default_factory=list)
|
|
230
232
|
|
|
@@ -343,7 +345,7 @@ class IctrpSession:
|
|
|
343
345
|
)
|
|
344
346
|
self.results_state = state
|
|
345
347
|
self.last_html = html
|
|
346
|
-
self.
|
|
348
|
+
self.reported_counts = parse_reported_counts(html)
|
|
347
349
|
return html
|
|
348
350
|
|
|
349
351
|
# ---- step 3 -----------------------------------------------------------
|
|
@@ -388,7 +390,8 @@ class IctrpSession:
|
|
|
388
390
|
content_disposition=response.headers.get("content-disposition"),
|
|
389
391
|
body=response.content,
|
|
390
392
|
location=response.headers.get("location"),
|
|
391
|
-
|
|
393
|
+
reported_records=None if self.reported_counts is None else self.reported_counts.records,
|
|
394
|
+
reported_trials=None if self.reported_counts is None else self.reported_counts.trials,
|
|
392
395
|
)
|
|
393
396
|
|
|
394
397
|
def provenance_steps(self) -> list[str]:
|
|
@@ -3,15 +3,22 @@
|
|
|
3
3
|
Every tool response carries this. The point is that a caller can always answer:
|
|
4
4
|
where did this come from, when did WHO last process it, and what is missing.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Three counts are kept rigorously separate, because conflating them is the most
|
|
7
7
|
likely way for this service to mislead:
|
|
8
8
|
|
|
9
|
-
* `rows_returned` -- what we actually hold.
|
|
10
|
-
* `
|
|
9
|
+
* `rows_returned` -- what we actually hold: one row per TRIAL.
|
|
10
|
+
* `upstream_reported_trials` -- what the portal claimed, after grouping records
|
|
11
|
+
that point at each other via Secondary ID. This is the number `rows_returned`
|
|
12
|
+
is comparable to.
|
|
13
|
+
* `upstream_reported_records` -- what the portal claimed before that grouping,
|
|
14
|
+
i.e. registry ROWS. A trial registered in five registries is five records and
|
|
15
|
+
one trial. This number is NEVER comparable to `rows_returned`, and the gap
|
|
16
|
+
between the two is duplicate registrations, not missing trials.
|
|
11
17
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
Reading the records figure as a trial figure is what produced the false
|
|
19
|
+
"the export is short by up to 29%" warning shipped in 0.1.2; see
|
|
20
|
+
docs/VERIFICATION_29PCT.md. `completeness` is now the single field a caller
|
|
21
|
+
needs, and it is `trials_complete` for every set we have measured.
|
|
15
22
|
"""
|
|
16
23
|
|
|
17
24
|
from __future__ import annotations
|
|
@@ -31,15 +38,31 @@ ATTRIBUTION = (
|
|
|
31
38
|
"WHO updates ICTRP weekly."
|
|
32
39
|
)
|
|
33
40
|
|
|
34
|
-
#: Stated on every response so the
|
|
41
|
+
#: Stated on every response so the unit question can never be missed.
|
|
35
42
|
INCOMPLETENESS_NOTICE = (
|
|
36
|
-
"The ICTRP
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
43
|
+
"The ICTRP portal reports two different counts: a RECORD count (registry rows, "
|
|
44
|
+
"where one trial may appear once per registry) and a TRIAL count (after the "
|
|
45
|
+
"portal groups records that reference each other via Secondary ID). This "
|
|
46
|
+
"export carries one row per TRIAL, so its row count is comparable to the "
|
|
47
|
+
"portal's trial count, not to its record count. Where the two differ, the "
|
|
48
|
+
"difference is duplicate registration records, not missing trials. Treat "
|
|
49
|
+
"counts from this service as counts of retrieved trials, and read a trial "
|
|
50
|
+
"absent from this set as not established rather than as not existing."
|
|
41
51
|
)
|
|
42
52
|
|
|
53
|
+
#: Stated only when the row count is genuinely below the portal's trial count.
|
|
54
|
+
SHORTFALL_NOTICE = (
|
|
55
|
+
"WARNING: this export holds FEWER ROWS than the number of trials the portal "
|
|
56
|
+
"reports for this search. That is a genuine shortfall, not a unit difference: "
|
|
57
|
+
"some trials the portal lists are absent from the export. Do not present these "
|
|
58
|
+
"counts as a complete set of matching trials."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
#: `completeness` values.
|
|
62
|
+
COMPLETE = "trials_complete"
|
|
63
|
+
SHORT = "trials_short"
|
|
64
|
+
UNKNOWN = "unknown"
|
|
65
|
+
|
|
43
66
|
|
|
44
67
|
@dataclass
|
|
45
68
|
class Provenance:
|
|
@@ -48,10 +71,12 @@ class Provenance:
|
|
|
48
71
|
source: str = SOURCE_NAME
|
|
49
72
|
source_url: str = SOURCE_URL
|
|
50
73
|
retrieved_at: str = ""
|
|
51
|
-
|
|
74
|
+
upstream_reported_records: int | None = None
|
|
75
|
+
upstream_reported_trials: int | None = None
|
|
52
76
|
rows_returned: int | None = None
|
|
53
|
-
|
|
54
|
-
|
|
77
|
+
completeness: str = UNKNOWN
|
|
78
|
+
rows_short_of_trials: int | None = None
|
|
79
|
+
duplicate_records: int | None = None
|
|
55
80
|
ictrp_export_date: str | None = None
|
|
56
81
|
ictrp_last_refreshed: str | None = None
|
|
57
82
|
request_steps: list[str] = field(default_factory=list)
|
|
@@ -85,37 +110,66 @@ class Provenance:
|
|
|
85
110
|
Anything else this class does not model is preserved in `extra` rather
|
|
86
111
|
than discarded, so a round-trip does not lose provenance detail that
|
|
87
112
|
some other layer added.
|
|
113
|
+
|
|
114
|
+
A 0.1.2 provenance is migrated rather than trusted: its single
|
|
115
|
+
`upstream_reported_total` was the portal's RECORD count, so it becomes
|
|
116
|
+
`upstream_reported_records` with no trial figure. `completeness`
|
|
117
|
+
therefore degrades to `unknown` instead of reproducing the old false
|
|
118
|
+
shortfall. Anything still cached under the old key is kept in `extra`,
|
|
119
|
+
so nothing is silently lost.
|
|
88
120
|
"""
|
|
89
121
|
known = {
|
|
90
|
-
"source", "source_url", "retrieved_at",
|
|
91
|
-
"
|
|
122
|
+
"source", "source_url", "retrieved_at",
|
|
123
|
+
"upstream_reported_records", "upstream_reported_trials",
|
|
124
|
+
"rows_returned", "completeness", "rows_short_of_trials",
|
|
125
|
+
"duplicate_records",
|
|
92
126
|
"ictrp_export_date", "ictrp_last_refreshed", "request_steps",
|
|
93
127
|
"cache_hit", "notes",
|
|
94
128
|
}
|
|
95
129
|
# Derived on every serialization, so never re-adopted from input.
|
|
96
|
-
derived = {"attribution", "incompleteness_notice"}
|
|
130
|
+
derived = {"attribution", "incompleteness_notice", "shortfall_notice"}
|
|
131
|
+
legacy = {"upstream_reported_total", "records_incomplete", "estimated_missing"}
|
|
97
132
|
extra = {
|
|
98
133
|
k: v
|
|
99
134
|
for k, v in data.items()
|
|
100
|
-
if k not in known and k not in derived
|
|
135
|
+
if k not in known and k not in derived and k not in legacy
|
|
101
136
|
}
|
|
102
|
-
|
|
137
|
+
fields = {k: v for k, v in data.items() if k in known}
|
|
138
|
+
if "upstream_reported_records" not in fields:
|
|
139
|
+
old_total = data.get("upstream_reported_total")
|
|
140
|
+
if isinstance(old_total, int):
|
|
141
|
+
# The old value IS the record count, just mislabelled. Migrate
|
|
142
|
+
# the number and drop the derived claims built on it.
|
|
143
|
+
fields["upstream_reported_records"] = old_total
|
|
144
|
+
# Legacy keys are kept only under a `legacy_` prefix, never under their
|
|
145
|
+
# original names: a caller reading `records_incomplete` must not keep
|
|
146
|
+
# seeing the false claim that shipped in 0.1.2, and a caller reading
|
|
147
|
+
# `upstream_reported_total` must not keep reading a record count as if
|
|
148
|
+
# it were a trial count.
|
|
149
|
+
for key in legacy:
|
|
150
|
+
if key in data:
|
|
151
|
+
extra[f"legacy_{key}"] = data[key]
|
|
152
|
+
return cls(**fields, extra=extra)
|
|
103
153
|
|
|
104
154
|
def to_dict(self) -> dict[str, Any]:
|
|
105
|
-
"""Serialized form. Attribution and the
|
|
155
|
+
"""Serialized form. Attribution and the unit notice always ride along."""
|
|
106
156
|
out: dict[str, Any] = {
|
|
107
157
|
"source": self.source,
|
|
108
158
|
"source_url": self.source_url,
|
|
109
159
|
"retrieved_at": self.retrieved_at,
|
|
110
160
|
"attribution": ATTRIBUTION,
|
|
111
161
|
}
|
|
112
|
-
if self.
|
|
113
|
-
out["
|
|
162
|
+
if self.upstream_reported_records is not None:
|
|
163
|
+
out["upstream_reported_records"] = self.upstream_reported_records
|
|
164
|
+
if self.upstream_reported_trials is not None:
|
|
165
|
+
out["upstream_reported_trials"] = self.upstream_reported_trials
|
|
114
166
|
if self.rows_returned is not None:
|
|
115
167
|
out["rows_returned"] = self.rows_returned
|
|
116
|
-
out["
|
|
117
|
-
if self.
|
|
118
|
-
out["
|
|
168
|
+
out["completeness"] = self.completeness
|
|
169
|
+
if self.rows_short_of_trials is not None:
|
|
170
|
+
out["rows_short_of_trials"] = self.rows_short_of_trials
|
|
171
|
+
if self.duplicate_records is not None:
|
|
172
|
+
out["duplicate_records"] = self.duplicate_records
|
|
119
173
|
if self.ictrp_export_date:
|
|
120
174
|
out["ictrp_export_date"] = self.ictrp_export_date
|
|
121
175
|
if self.ictrp_last_refreshed:
|
|
@@ -123,12 +177,9 @@ class Provenance:
|
|
|
123
177
|
if self.request_steps:
|
|
124
178
|
out["request_steps"] = list(self.request_steps)
|
|
125
179
|
out["cache_hit"] = self.cache_hit
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
# Even a complete-looking response carries the caveat: we can never
|
|
130
|
-
# prove from a single export that nothing was withheld.
|
|
131
|
-
out["incompleteness_notice"] = INCOMPLETENESS_NOTICE
|
|
180
|
+
out["incompleteness_notice"] = INCOMPLETENESS_NOTICE
|
|
181
|
+
if self.completeness == SHORT:
|
|
182
|
+
out["shortfall_notice"] = SHORTFALL_NOTICE
|
|
132
183
|
if self.notes:
|
|
133
184
|
out["notes"] = list(self.notes)
|
|
134
185
|
# Keys this class does not model, restored verbatim.
|
|
@@ -141,11 +192,28 @@ def _utc_now() -> str:
|
|
|
141
192
|
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
142
193
|
|
|
143
194
|
|
|
195
|
+
def classify_completeness(
|
|
196
|
+
rows: int | None, reported_trials: int | None
|
|
197
|
+
) -> tuple[str, int | None]:
|
|
198
|
+
"""Decide whether the export really is short of what the portal promised.
|
|
199
|
+
|
|
200
|
+
Returns `(completeness, rows_short_of_trials)`. `rows_short_of_trials` is
|
|
201
|
+
None unless we actually know the portal's trial count, so `unknown` never
|
|
202
|
+
carries a fabricated shortfall.
|
|
203
|
+
"""
|
|
204
|
+
if reported_trials is None:
|
|
205
|
+
return UNKNOWN, None
|
|
206
|
+
if rows is not None and rows < reported_trials:
|
|
207
|
+
return SHORT, reported_trials - rows
|
|
208
|
+
return COMPLETE, None
|
|
209
|
+
|
|
210
|
+
|
|
144
211
|
def derive_set_provenance(
|
|
145
212
|
*,
|
|
146
213
|
keyword: str,
|
|
147
214
|
trials: list[dict[str, Any]],
|
|
148
|
-
|
|
215
|
+
reported_records: int | None,
|
|
216
|
+
reported_trials: int | None,
|
|
149
217
|
response_date: str | None,
|
|
150
218
|
steps: list[str],
|
|
151
219
|
) -> Provenance:
|
|
@@ -158,13 +226,22 @@ def derive_set_provenance(
|
|
|
158
226
|
{t.get("last_refreshed_display") for t in trials if t.get("last_refreshed_display")}
|
|
159
227
|
)
|
|
160
228
|
|
|
229
|
+
completeness, short = classify_completeness(rows, reported_trials)
|
|
230
|
+
# Only meaningful when the portal gave us both units: then the gap is
|
|
231
|
+
# exactly the number of records it counted more than once.
|
|
232
|
+
duplicates = (
|
|
233
|
+
reported_records - reported_trials
|
|
234
|
+
if reported_records is not None and reported_trials is not None
|
|
235
|
+
else None
|
|
236
|
+
)
|
|
237
|
+
|
|
161
238
|
provenance = Provenance.now(
|
|
162
|
-
|
|
239
|
+
upstream_reported_records=reported_records,
|
|
240
|
+
upstream_reported_trials=reported_trials,
|
|
163
241
|
rows_returned=rows,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
),
|
|
242
|
+
completeness=completeness,
|
|
243
|
+
rows_short_of_trials=short,
|
|
244
|
+
duplicate_records=duplicates,
|
|
168
245
|
ictrp_export_date=export_dates[0] if export_dates else None,
|
|
169
246
|
ictrp_last_refreshed=refreshed[0] if refreshed else None,
|
|
170
247
|
request_steps=list(steps),
|
|
@@ -172,6 +249,24 @@ def derive_set_provenance(
|
|
|
172
249
|
)
|
|
173
250
|
|
|
174
251
|
provenance.notes.append(f"Search keyword: {keyword!r}.")
|
|
252
|
+
provenance.notes.append(
|
|
253
|
+
f"Export rows: {rows}. Portal reports {reported_trials} trials"
|
|
254
|
+
if reported_trials is not None
|
|
255
|
+
else f"Export rows: {rows}. The portal's trial count was not readable, "
|
|
256
|
+
"so completeness is unknown."
|
|
257
|
+
)
|
|
258
|
+
if duplicates:
|
|
259
|
+
provenance.notes.append(
|
|
260
|
+
f"The portal reports {reported_records} records for {reported_trials} "
|
|
261
|
+
f"trials; the {duplicates}-record difference is cross-registry "
|
|
262
|
+
"duplicate registration of the same trials, not missing data. This "
|
|
263
|
+
"export keeps one row per trial."
|
|
264
|
+
)
|
|
265
|
+
if completeness == SHORT:
|
|
266
|
+
provenance.notes.append(
|
|
267
|
+
f"This export is short by {short} trial(s) against the portal's own "
|
|
268
|
+
"trial count. That is a real shortfall and is reported as one."
|
|
269
|
+
)
|
|
175
270
|
if refreshed:
|
|
176
271
|
provenance.notes.append(
|
|
177
272
|
f"Record-level 'Last Refreshed on' values span {len(refreshed)} distinct "
|
|
@@ -179,4 +274,4 @@ def derive_set_provenance(
|
|
|
179
274
|
)
|
|
180
275
|
if response_date:
|
|
181
276
|
provenance.notes.append(f"Upstream response Date header: {response_date}.")
|
|
182
|
-
return provenance
|
|
277
|
+
return provenance
|
|
@@ -31,20 +31,42 @@ from .data.columns import CSV_COLUMNS
|
|
|
31
31
|
from .data.jsonio import snapshot_from_trials
|
|
32
32
|
from .errors import ErrorCode, IctrpError
|
|
33
33
|
from .ictrp.session import IctrpSession
|
|
34
|
-
from .provenance import
|
|
34
|
+
from .provenance import (
|
|
35
|
+
INCOMPLETENESS_NOTICE,
|
|
36
|
+
SHORTFALL_NOTICE,
|
|
37
|
+
Provenance,
|
|
38
|
+
derive_set_provenance,
|
|
39
|
+
)
|
|
35
40
|
|
|
36
41
|
#: Fields shown by default when a caller does not ask for specific ones. Chosen to
|
|
37
42
|
#: be the high-coverage, decision-relevant columns rather than all 58.
|
|
43
|
+
#: The default view. Deliberately NOT just the 10 most common columns: it is the
|
|
44
|
+
#: view a clinician actually needs to triage a target -- name, sponsor/PI, where
|
|
45
|
+
#: it runs, eligibility shape, and a treatment-line hint. Sponsor/PI come from the
|
|
46
|
+
#: `primary_sponsor` + `contact_*` columns (ICTRP has no dedicated "PI" or
|
|
47
|
+
#: "participating-hospital" field; hospitals exist only as `countries` and the
|
|
48
|
+
#: contact's affiliation), so those are surfaced here. `line_of_therapy_hint` is a
|
|
49
|
+
#: non-authoritative extraction from the free-text criteria -- see normalize.py.
|
|
38
50
|
DEFAULT_FIELDS: tuple[str, ...] = (
|
|
39
51
|
"trial_id",
|
|
40
52
|
"source_register",
|
|
41
53
|
"public_title",
|
|
54
|
+
"scientific_title",
|
|
55
|
+
"primary_sponsor",
|
|
56
|
+
"secondary_sponsor",
|
|
57
|
+
"contact_firstname",
|
|
58
|
+
"contact_lastname",
|
|
59
|
+
"contact_affiliation",
|
|
42
60
|
"recruitment_status",
|
|
43
61
|
"phase_code",
|
|
44
62
|
"registration_date",
|
|
45
63
|
"condition",
|
|
46
64
|
"countries",
|
|
65
|
+
"inclusion_age_min",
|
|
66
|
+
"inclusion_age_max",
|
|
67
|
+
"inclusion_gender",
|
|
47
68
|
"target_size_total",
|
|
69
|
+
"line_of_therapy_hint",
|
|
48
70
|
"last_refreshed_display",
|
|
49
71
|
)
|
|
50
72
|
|
|
@@ -166,10 +188,12 @@ class IctrpService:
|
|
|
166
188
|
|
|
167
189
|
provenance = Provenance.from_dict(materialized.provenance) if materialized.provenance else Provenance.now()
|
|
168
190
|
provenance.cache_hit = cache_hit
|
|
191
|
+
provenance.upstream_reported_records = materialized.reported_records
|
|
192
|
+
provenance.upstream_reported_trials = materialized.reported_trials
|
|
169
193
|
provenance.rows_returned = materialized.rows_returned
|
|
170
|
-
provenance.
|
|
171
|
-
provenance.
|
|
172
|
-
provenance.
|
|
194
|
+
provenance.completeness = materialized.completeness
|
|
195
|
+
provenance.rows_short_of_trials = materialized.rows_short_of_trials
|
|
196
|
+
provenance.duplicate_records = materialized.duplicate_records
|
|
173
197
|
self._annotate_age(provenance, materialized)
|
|
174
198
|
if refresh_error:
|
|
175
199
|
provenance.notes.append(
|
|
@@ -181,9 +205,11 @@ class IctrpService:
|
|
|
181
205
|
"status": "ok",
|
|
182
206
|
"set_id": materialized.set_id,
|
|
183
207
|
"matched_rows_returned": len(ordered),
|
|
184
|
-
"
|
|
185
|
-
"
|
|
186
|
-
"
|
|
208
|
+
"upstream_reported_records": materialized.reported_records,
|
|
209
|
+
"upstream_reported_trials": materialized.reported_trials,
|
|
210
|
+
"completeness": materialized.completeness,
|
|
211
|
+
"rows_short_of_trials": materialized.rows_short_of_trials,
|
|
212
|
+
"duplicate_records": materialized.duplicate_records,
|
|
187
213
|
"counts_are_of_retrieved_rows_not_of_matching_trials": True,
|
|
188
214
|
"offset": offset,
|
|
189
215
|
"limit": limit,
|
|
@@ -274,33 +300,31 @@ class IctrpService:
|
|
|
274
300
|
finally:
|
|
275
301
|
await session.aclose()
|
|
276
302
|
|
|
303
|
+
# Build trial dicts first. Deriving provenance needs the real row count:
|
|
304
|
+
# derived against an empty list it would compare 0 rows against the
|
|
305
|
+
# portal's trial count and record a catastrophic shortfall for an export
|
|
306
|
+
# that is in fact complete.
|
|
307
|
+
from .data.normalize import to_trial
|
|
308
|
+
|
|
309
|
+
trials = [to_trial(row, payload.header) for row in payload.rows]
|
|
310
|
+
|
|
277
311
|
provenance = derive_set_provenance(
|
|
278
312
|
keyword=keyword,
|
|
279
|
-
trials=
|
|
280
|
-
|
|
313
|
+
trials=trials,
|
|
314
|
+
reported_records=payload.reported_records,
|
|
315
|
+
reported_trials=payload.reported_trials,
|
|
281
316
|
response_date=response_date,
|
|
282
317
|
steps=steps,
|
|
283
318
|
)
|
|
284
319
|
|
|
285
|
-
# Build trial dicts once to derive export/last-refreshed dates for provenance.
|
|
286
|
-
from .data.normalize import to_trial
|
|
287
|
-
|
|
288
|
-
trials = [to_trial(row, payload.header) for row in payload.rows]
|
|
289
|
-
export_dates = sorted({t.get("export_date_raw") for t in trials if t.get("export_date_raw")})
|
|
290
|
-
refreshed = sorted(
|
|
291
|
-
{t.get("last_refreshed_display") for t in trials if t.get("last_refreshed_display")}
|
|
292
|
-
)
|
|
293
|
-
if export_dates:
|
|
294
|
-
provenance.ictrp_export_date = export_dates[0]
|
|
295
|
-
provenance.notes = [n for n in provenance.notes if "Export date" not in n]
|
|
296
|
-
|
|
297
320
|
raw = _serialize_csv(payload.header, payload.rows)
|
|
298
321
|
materialized = self.store.store(
|
|
299
322
|
keyword=keyword,
|
|
300
323
|
payload_raw=raw,
|
|
301
324
|
header=payload.header,
|
|
302
325
|
rows=payload.rows,
|
|
303
|
-
|
|
326
|
+
reported_records=payload.reported_records,
|
|
327
|
+
reported_trials=payload.reported_trials,
|
|
304
328
|
provenance=provenance.to_dict(),
|
|
305
329
|
)
|
|
306
330
|
|
|
@@ -344,9 +368,11 @@ class IctrpService:
|
|
|
344
368
|
provenance = Provenance.from_dict(materialized.provenance)
|
|
345
369
|
provenance.cache_hit = True
|
|
346
370
|
provenance.rows_returned = materialized.rows_returned
|
|
347
|
-
provenance.
|
|
348
|
-
provenance.
|
|
349
|
-
provenance.
|
|
371
|
+
provenance.upstream_reported_records = materialized.reported_records
|
|
372
|
+
provenance.upstream_reported_trials = materialized.reported_trials
|
|
373
|
+
provenance.completeness = materialized.completeness
|
|
374
|
+
provenance.rows_short_of_trials = materialized.rows_short_of_trials
|
|
375
|
+
provenance.duplicate_records = materialized.duplicate_records
|
|
350
376
|
|
|
351
377
|
return {
|
|
352
378
|
"status": "ok",
|
|
@@ -402,8 +428,11 @@ class IctrpService:
|
|
|
402
428
|
"status": "ok",
|
|
403
429
|
"set_id": materialized.set_id,
|
|
404
430
|
"rows_in_set": materialized.rows_returned,
|
|
405
|
-
"
|
|
406
|
-
"
|
|
431
|
+
"upstream_reported_records": materialized.reported_records,
|
|
432
|
+
"upstream_reported_trials": materialized.reported_trials,
|
|
433
|
+
"completeness": materialized.completeness,
|
|
434
|
+
"rows_short_of_trials": materialized.rows_short_of_trials,
|
|
435
|
+
"duplicate_records": materialized.duplicate_records,
|
|
407
436
|
"facets": facets,
|
|
408
437
|
"field_coverage": q.field_coverage(
|
|
409
438
|
materialized.trials,
|
|
@@ -496,10 +525,21 @@ class IctrpService:
|
|
|
496
525
|
f"# Retrieved: {provenance.retrieved_at}\n"
|
|
497
526
|
f"# Rows in this export: {len(rows)}\n"
|
|
498
527
|
)
|
|
499
|
-
if provenance.
|
|
500
|
-
header +=
|
|
501
|
-
|
|
502
|
-
|
|
528
|
+
if provenance.upstream_reported_records is not None:
|
|
529
|
+
header += (
|
|
530
|
+
f"# Portal-reported records: {provenance.upstream_reported_records} "
|
|
531
|
+
"(registry rows; one trial may be registered in several)\n"
|
|
532
|
+
)
|
|
533
|
+
if provenance.upstream_reported_trials is not None:
|
|
534
|
+
header += (
|
|
535
|
+
f"# Portal-reported trials: {provenance.upstream_reported_trials} "
|
|
536
|
+
"(grouped by Secondary ID; comparable to the row count)\n"
|
|
537
|
+
)
|
|
538
|
+
header += f"# Completeness: {provenance.completeness}\n"
|
|
539
|
+
if provenance.completeness == "trials_short":
|
|
540
|
+
header += f"# WARNING: {SHORTFALL_NOTICE}\n"
|
|
541
|
+
else:
|
|
542
|
+
header += f"# NOTE: {INCOMPLETENESS_NOTICE}\n"
|
|
503
543
|
|
|
504
544
|
return {
|
|
505
545
|
"status": "ok",
|
|
@@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta"
|
|
|
10
10
|
|
|
11
11
|
[project]
|
|
12
12
|
name = "ictrp-mcp-service"
|
|
13
|
-
version = "0.
|
|
13
|
+
version = "0.2.0"
|
|
14
14
|
description = "MCP server exposing the WHO ICTRP clinical trials database over pure HTTP"
|
|
15
15
|
requires-python = ">=3.10"
|
|
16
16
|
license = { text = "MIT" }
|