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 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 is incomplete, and it fails silently.** Measured against
110
- the portal's own reported match counts:
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 | Portal reports | CSV delivers | Shortfall |
112
+ | Query | Records the portal reports | Trials the portal reports | Rows the CSV holds |
113
113
  |---|---|---|---|
114
- | `ChiCTR` | 14,197 | 14,147 | 0.4% |
115
- | `pancreatic` | 10,354 | 9,273 | 10.4% |
116
- | `pancreatic cancer` | 6,952 | 6,262 | 9.9% |
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
- This is not a counting artifact: paginating the HTML results for `KRAS` yielded
120
- 108 distinct trial IDs, **8 of which were absent from the CSV** — records
121
- visible on the results page and missing from the export. The cause is unknown;
122
- duplicate records, a fixed row cap, and pagination limits have all been ruled
123
- out.
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`, and separately
131
- `upstream_reported_total`, and they are never merged.
132
- - Every response carries `records_incomplete` and an `incompleteness_notice`.
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
- | 关键词 | 门户报告 | CSV 实际给出 | 缺口 |
104
+ | 关键词 | 门户报告的记录数 | 门户报告的试验数 | CSV 实际给出的行数 |
104
105
  |---|---|---|---|
105
- | `ChiCTR` | 14,197 | 14,147 | 0.4% |
106
- | `pancreatic` | 10,354 | 9,273 | 10.4% |
107
- | `pancreatic cancer` | 6,952 | 6,262 | 9.9% |
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
- 这不是统计口径的差异:对 `KRAS` 的结果页做 HTML 分页得到 108 个互不相同的试验 ID,
111
- 其中 **8 个在 CSV 里不存在**——它们在结果页上可见,却没有出现在导出中。原因未知;
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
- `upstream_reported_total`,两者永不合并。
120
- - 每个响应都带 `records_incomplete` 与 `incompleteness_notice`。
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 incompleteness accounting, 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.** ICTRP's export silently omits
12
- * records, so an empty answer and a failed answer are dangerously easy to
13
- * confuse. Failures are therefore reported with `isError: true` and a
14
- * machine-readable `error_code`, and the incompleteness notice is attached to
15
- * every successful result that carries rows.
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 incompleteness accounting, 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.** ICTRP's export silently omits
12
- * records, so an empty answer and a failed answer are dangerously easy to
13
- * confuse. Failures are therefore reported with `isError: true` and a
14
- * machine-readable `error_code`, and the incompleteness notice is attached to
15
- * every successful result that carries rows.
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.1.1";
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 warning will
29
- * say "there are 12 trials"; the measured shortfall against the portal's own
30
- * counts reaches 29% for some queries, so that sentence can be badly wrong.
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 = "Counts describe retrieved rows only. The ICTRP CSV export is known to omit records " +
33
- "the portal itself reports as matches, so absence is not evidence of nonexistence.";
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":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ictrp-mcp-server",
3
- "version": "0.1.1",
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",
@@ -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 Provenance # noqa: E402
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
- # Repeated verbatim on every row-bearing response. The Node layer also attaches
94
- # it, but the sidecar must not depend on the caller remembering: anything that
95
- # reads this HTTP API directly deserves the same warning.
96
- _INCOMPLETENESS_NOTICE = (
97
- "Counts describe retrieved rows only. The ICTRP CSV export is known to omit "
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
- return {
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": len(trials),
374
- "records_incomplete": True,
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
- reported_total: int | None
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
- Never described as a total. The portal's own figure lives separately in
81
- `reported_total`, and the two are known to differ by 0.4%-29%.
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 is_incomplete(self) -> bool:
87
- """True when the portal reported more matches than the export delivered."""
88
- if self.reported_total is None:
89
- return False
90
- return self.reported_total > self.rows_returned
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 missing_estimate(self) -> int | None:
94
- if self.reported_total is None:
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.reported_total - self.rows_returned)
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
- "upstream_reported_total": self.reported_total,
107
- "records_incomplete": self.is_incomplete,
108
- "estimated_missing": self.missing_estimate,
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
- reported_total: int | None,
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
- reported_total=reported_total,
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
- "reported_total": reported_total,
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
- reported_total=provenance.get("upstream_reported_total"),
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
- reported_total=meta.get("reported_total"),
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
- reported_total: int | None = None
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
- reported_total: int | None = None,
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
- reported_total=reported_total,
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
- _RECORDS_RE = re.compile(r"([\d,]+)\s*records", re.I)
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
- def parse_reported_total(html: str) -> int | None:
257
- """Extract the portal's own 'N records' figure from a results page.
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
- This is the portal's claim, NOT a verified count of what we received. The
260
- two routinely differ -- measured shortfalls of 0.4% to 29% -- so callers must
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(match.group(1).replace(",", ""))
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
- parse_reported_total,
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
- reported_total: int | None = None
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.reported_total = parse_reported_total(html)
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
- reported_total=self.reported_total,
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
- Two things are kept rigorously separate, because conflating them is the most
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
- * `upstream_reported_total` -- what the portal claimed matched.
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
- The export is provably incomplete (docs/MEASUREMENTS.md section 2), so neither
13
- number alone may be presented as "how many trials exist". When they disagree, the
14
- response says so explicitly.
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 incompleteness can never be missed.
41
+ #: Stated on every response so the unit question can never be missed.
35
42
  INCOMPLETENESS_NOTICE = (
36
- "The ICTRP CSV export is known to omit records that the search portal itself "
37
- "reports as matches (measured shortfalls from 0.4% to 29% depending on query). "
38
- "A record absent from a result set is therefore NOT evidence that it does not "
39
- "exist. Treat counts from this service as counts of retrieved rows, not as "
40
- "complete counts of matching trials."
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
- upstream_reported_total: int | None = None
74
+ upstream_reported_records: int | None = None
75
+ upstream_reported_trials: int | None = None
52
76
  rows_returned: int | None = None
53
- records_incomplete: bool = False
54
- estimated_missing: int | None = None
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", "upstream_reported_total",
91
- "rows_returned", "records_incomplete", "estimated_missing",
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
- return cls(**{k: v for k, v in data.items() if k in known}, extra=extra)
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 incompleteness notice always ride along."""
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.upstream_reported_total is not None:
113
- out["upstream_reported_total"] = self.upstream_reported_total
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["records_incomplete"] = self.records_incomplete
117
- if self.estimated_missing is not None:
118
- out["estimated_missing"] = self.estimated_missing
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
- if self.records_incomplete:
127
- out["incompleteness_notice"] = INCOMPLETENESS_NOTICE
128
- elif not self.cache_hit:
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
- reported_total: int | None,
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
- upstream_reported_total=reported_total,
239
+ upstream_reported_records=reported_records,
240
+ upstream_reported_trials=reported_trials,
163
241
  rows_returned=rows,
164
- records_incomplete=bool(reported_total is not None and reported_total > rows),
165
- estimated_missing=(
166
- max(0, reported_total - rows) if reported_total is not None else None
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 INCOMPLETENESS_NOTICE, Provenance, derive_set_provenance
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.upstream_reported_total = materialized.reported_total
171
- provenance.records_incomplete = materialized.is_incomplete
172
- provenance.estimated_missing = materialized.missing_estimate
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
- "upstream_reported_total": materialized.reported_total,
185
- "records_incomplete": materialized.is_incomplete,
186
- "estimated_missing": materialized.missing_estimate,
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=[], # filled in by the store; only used for date extraction below
280
- reported_total=payload.reported_total,
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
- reported_total=payload.reported_total,
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.upstream_reported_total = materialized.reported_total
348
- provenance.records_incomplete = materialized.is_incomplete
349
- provenance.estimated_missing = materialized.missing_estimate
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
- "upstream_reported_total": materialized.reported_total,
406
- "records_incomplete": materialized.is_incomplete,
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.upstream_reported_total is not None:
500
- header += f"# Portal-reported matches: {provenance.upstream_reported_total}\n"
501
- if provenance.records_incomplete:
502
- header += f"# WARNING: {INCOMPLETENESS_NOTICE}\n"
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.1.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" }