← Files JinkōARCHIVED FILE
skills/jinko-task-trial-data-scoping/scripts/compile_trials.py
3.68 KB · Oct 5, 2026 · 18:30 UTC
#!/usr/bin/env python3
"""Merge normalized ClinicalTrials.gov angle outputs by NCT ID."""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
from typing import Any
try:
from common import write_json
except ImportError: # pragma: no cover
from .common import write_json
def load_json(path: Path) -> Any:
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise ValueError(f"Cannot read {path}: {exc}") from exc
def parse_angle(value: str) -> tuple[str, Path]:
if "=" not in value:
raise ValueError(f"Invalid --angle {value!r}; expected LABEL=FILE")
label, raw_path = value.split("=", 1)
if not label or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", label):
raise ValueError(f"Invalid angle label {label!r}")
return label, Path(raw_path)
def merge_missing(target: dict[str, Any], source: dict[str, Any]) -> None:
for key, value in source.items():
if key in {"query_provenance", "triangulation_count"}:
continue
if not target.get(key) and value:
target[key] = value
target["record_completeness"] = max(
int(target.get("record_completeness") or 0),
int(source.get("record_completeness") or 0),
)
def compile_trials(angles: list[tuple[str, Path]]) -> list[dict[str, Any]]:
merged: list[dict[str, Any]] = []
by_nct: dict[str, dict[str, Any]] = {}
for label, path in angles:
payload = load_json(path)
if not isinstance(payload, dict) or not isinstance(
payload.get("records"), list
):
raise ValueError(f"{path} must contain a records array")
query = str(payload.get("query") or "")
for record in payload["records"]:
if not isinstance(record, dict):
raise ValueError(f"{path} contains a non-object record")
nct_id = str(record.get("nct_id") or "").strip().upper()
if not nct_id:
continue
current = by_nct.get(nct_id)
if current is None:
current = dict(record)
current["query_provenance"] = []
by_nct[nct_id] = current
merged.append(current)
else:
merge_missing(current, record)
provenance = {"angle": query, "label": label}
if provenance not in current["query_provenance"]:
current["query_provenance"].append(provenance)
for record in merged:
record["triangulation_count"] = len(record["query_provenance"])
return sorted(
merged,
key=lambda record: (
int(record.get("triangulation_count") or 0),
bool(record.get("has_results")),
int(record.get("record_completeness") or 0),
),
reverse=True,
)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--angle", action="append", required=True, help="Angle as LABEL=FILE."
)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
try:
angles = [parse_angle(value) for value in args.angle]
labels = [label for label, _ in angles]
if len(labels) != len(set(labels)):
raise ValueError("Angle labels must be unique")
records = compile_trials(angles)
write_json(args.output, records)
print(json.dumps({"candidates": len(records), "output": str(args.output)}))
return 0
except ValueError as exc:
print(str(exc), file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())
SHA-256: 9dc19a86b54bdc8f4090fa8a0807a08ad6c31f0c66222e6c97280c85c05800ad