Skills › Scientific Agent Skills › scientific-writing › select_reporting_guidelines.py
select_reporting_guidelines.py
214 lines · skills/scientific-writing/scripts/select_reporting_guidelines.py at 5f2bc03
1"""Select reporting guidance and check non-scoring coverage metadata."""
3from __future__ import annotations
5import argparse
6from pathlib import Path
7from typing import Any
9from _common import (
10 InputError,
11 Issue,
12 emit_report,
13 is_nonempty_string,
14 issue,
15 read_json,
16 require_list,
17 require_object,
18 run,
19)
21TOOL = "select_reporting_guidelines"
22REGISTRY_PATH = (
23 Path(__file__).resolve().parents[1] / "assets" / "reporting_guidelines.json"
24)
25DISCLAIMER = (
26 "Selection and coverage are non-scoring aids. They do not reproduce an official "
27 "checklist, appraise study quality, certify compliance, or replace the current "
28 "guideline and target-journal instructions."
29)
32def load_registry() -> dict[str, dict[str, Any]]:
33 root = require_object(read_json(REGISTRY_PATH), "registry")
34 if root.get("schema_version") != "1.0":
35 raise InputError("unsupported reporting-guideline registry version")
36 raw_items = require_list(root.get("guidelines"), "guidelines")
37 registry: dict[str, dict[str, Any]] = {}
38 for raw_item in raw_items:
39 item = require_object(raw_item, "guideline")
40 guideline_id = item.get("id")
41 if not is_nonempty_string(guideline_id) or guideline_id in registry:
42 raise InputError("registry guideline IDs must be non-empty and unique")
43 registry[guideline_id] = item
44 return registry
47def _condition_matches(item: dict[str, Any], args: argparse.Namespace) -> bool:
48 conditions = require_object(item.get("conditions", {}), "conditions")
49 for flag in ("ai", "llm", "routinely_collected", "qualitative_component"):
50 required = conditions.get(flag)
51 if required is True and not getattr(args, flag):
52 return False
53 return True
56def select_guidelines(
57 registry: dict[str, dict[str, Any]],
58 args: argparse.Namespace,
59) -> tuple[list[str], list[str], list[Issue]]:
60 primary: list[str] = []
61 extensions: list[str] = []
62 issues: list[Issue] = []
63 for guideline_id, item in registry.items():
64 designs = require_list(
65 item.get("study_designs"), f"{guideline_id}.study_designs"
66 )
67 if args.study_design not in designs:
68 continue
69 if bool(item.get("protocol")) != bool(args.protocol):
70 continue
71 if not _condition_matches(item, args):
72 continue
73 if item.get("role") == "primary":
74 primary.append(guideline_id)
75 elif item.get("role") == "extension":
76 extensions.append(guideline_id)
78 if not primary:
79 issues.append(
80 issue("warning", "NO_PRIMARY_LOCAL_MATCH", item_id=args.study_design)
81 )
82 if args.llm and not args.ai:
83 issues.append(issue("error", "LLM_REQUIRES_AI_FLAG", location="arguments"))
84 return sorted(primary), sorted(extensions), issues
87def check_coverage(
88 registry: dict[str, dict[str, Any]],
89 coverage_path: str,
90) -> tuple[list[Issue], dict[str, Any]]:
91 data = require_object(read_json(coverage_path), "coverage")
92 if data.get("schema_version") != "1.0":
93 raise InputError("unsupported reporting-coverage schema version")
94 guideline_id = data.get("guideline_id")
95 if guideline_id not in registry:
96 raise InputError("coverage guideline_id is not in the bundled registry")
97 guideline = registry[guideline_id]
98 expected = {
99 item["id"]: item
100 for item in (
101 require_object(raw, "coverage_topic")
102 for raw in require_list(guideline.get("coverage_topics"), "coverage_topics")
103 )
104 }
106 issues: list[Issue] = []
107 observed: dict[str, dict[str, Any]] = {}
108 for index, raw_item in enumerate(require_list(data.get("items"), "items")):
109 item = require_object(raw_item, f"items[{index}]")
110 topic_id = item.get("topic_id")
111 if topic_id not in expected:
112 issues.append(
113 issue("error", "UNKNOWN_COVERAGE_TOPIC", location=f"items[{index}]")
114 )
115 continue
116 if topic_id in observed:
117 issues.append(
118 issue("error", "DUPLICATE_COVERAGE_TOPIC", item_id=str(topic_id))
119 )
120 continue
121 observed[str(topic_id)] = item
122 status = item.get("status")
123 if status not in {"addressed", "not_applicable", "missing"}:
124 issues.append(
125 issue("error", "INVALID_COVERAGE_STATUS", item_id=str(topic_id))
126 )
127 if status == "addressed":
128 locations = item.get("locations")
129 if (
130 not isinstance(locations, list)
131 or not locations
132 or not all(is_nonempty_string(value) for value in locations)
133 ):
134 issues.append(
135 issue(
136 "error",
137 "ADDRESSED_TOPIC_WITHOUT_LOCATION",
138 item_id=str(topic_id),
139 )
140 )
141 if status == "not_applicable" and not is_nonempty_string(item.get("rationale")):
142 issues.append(
143 issue(
144 "error", "NOT_APPLICABLE_WITHOUT_RATIONALE", item_id=str(topic_id)
145 )
146 )
147 if status == "missing":
148 issues.append(
149 issue("error", "COVERAGE_TOPIC_MISSING", item_id=str(topic_id))
150 )
152 for topic_id in sorted(set(expected) - set(observed)):
153 issues.append(issue("error", "COVERAGE_TOPIC_NOT_RECORDED", item_id=topic_id))
155 summary = {
156 "guideline_id": guideline_id,
157 "topics_expected": len(expected),
158 "topics_recorded": len(observed),
159 "coverage_status": (
160 "incomplete"
161 if any(item.severity == "error" for item in issues)
162 else "all_bundled_topics_addressed"
163 ),
164 "disclaimer": DISCLAIMER,
165 }
166 return issues, summary
169def build_parser() -> argparse.ArgumentParser:
170 parser = argparse.ArgumentParser(
171 description=(
172 "Use the bundled offline registry to select candidate reporting guidance "
173 "or validate a non-scoring coverage record."
174 )
175 )
176 subparsers = parser.add_subparsers(dest="command", required=True)
178 select_parser = subparsers.add_parser(
179 "select",
180 help="select candidate guidelines from study metadata",
181 )
182 select_parser.add_argument("--study-design", required=True)
183 select_parser.add_argument("--protocol", action="store_true")
184 select_parser.add_argument("--ai", action="store_true")
185 select_parser.add_argument("--llm", action="store_true")
186 select_parser.add_argument("--routinely-collected", action="store_true")
187 select_parser.add_argument("--qualitative-component", action="store_true")
189 check_parser = subparsers.add_parser(
190 "check",
191 help="validate high-level coverage metadata",
192 )
193 check_parser.add_argument("coverage", help="UTF-8 JSON coverage record")
194 return parser
197def cli() -> int:
198 args = build_parser().parse_args()
199 registry = load_registry()
200 if args.command == "select":
201 primary, extensions, issues = select_guidelines(registry, args)
202 summary = {
203 "study_design": args.study_design,
204 "primary": primary,
205 "extensions": extensions,
206 "disclaimer": DISCLAIMER,
207 }
208 else:
209 issues, summary = check_coverage(registry, args.coverage)
210 return emit_report(TOOL, issues, summary=summary)
213if __name__ == "__main__":
214 run(TOOL, cli)