medsci-skills 5.19.0 → 5.20.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/README.md +2 -0
- package/metadata/distribution_files.json +187 -12
- package/metadata/distribution_manifest.json +1 -1
- package/package.json +1 -1
- package/skills/peer-review/SKILL.md +10 -1
- package/skills/peer-review/references/domain-probes/diagnostic_accuracy.md +1 -0
- package/skills/peer-review/references/domain-probes/observational_confounding.md +1 -0
- package/skills/self-review/references/domain-probes/diagnostic_accuracy.md +1 -0
- package/skills/self-review/references/domain-probes/observational_confounding.md +1 -0
- package/skills/self-review/scripts/check_dta_denominators.py +182 -0
- package/skills/self-review/scripts/check_dta_denominators_challenge/expected/bad.txt +5 -0
- package/skills/self-review/scripts/check_dta_denominators_challenge/expected/ok.txt +3 -0
- package/skills/self-review/scripts/check_dta_denominators_challenge/fixture/dta_bad.md +12 -0
- package/skills/self-review/scripts/check_dta_denominators_challenge/fixture/dta_ok.md +12 -0
- package/skills/self-review/scripts/check_dta_denominators_challenge/problem.md +29 -0
- package/skills/self-review/scripts/check_dta_denominators_challenge/verify.sh +19 -0
- package/skills/self-review/scripts/check_nested_group_comparison.py +154 -0
- package/skills/self-review/scripts/check_nested_group_comparison_challenge/expected/clean.txt +3 -0
- package/skills/self-review/scripts/check_nested_group_comparison_challenge/expected/nested.txt +3 -0
- package/skills/self-review/scripts/check_nested_group_comparison_challenge/fixture/clean.md +11 -0
- package/skills/self-review/scripts/check_nested_group_comparison_challenge/fixture/nested.md +6 -0
- package/skills/self-review/scripts/check_nested_group_comparison_challenge/problem.md +27 -0
- package/skills/self-review/scripts/check_nested_group_comparison_challenge/verify.sh +20 -0
- package/skills/self-review/scripts/check_paired_difference_estimator.py +149 -0
- package/skills/self-review/scripts/check_paired_difference_estimator_challenge/expected/bad.txt +5 -0
- package/skills/self-review/scripts/check_paired_difference_estimator_challenge/expected/ok.txt +3 -0
- package/skills/self-review/scripts/check_paired_difference_estimator_challenge/fixture/paired_bad.md +8 -0
- package/skills/self-review/scripts/check_paired_difference_estimator_challenge/fixture/paired_ok.md +9 -0
- package/skills/self-review/scripts/check_paired_difference_estimator_challenge/problem.md +26 -0
- package/skills/self-review/scripts/check_paired_difference_estimator_challenge/verify.sh +20 -0
- package/skills/self-review/scripts/check_reported_p_from_counts.py +214 -0
- package/skills/self-review/scripts/check_reported_p_from_counts_challenge/expected/bad.txt +3 -0
- package/skills/self-review/scripts/check_reported_p_from_counts_challenge/expected/ok.txt +3 -0
- package/skills/self-review/scripts/check_reported_p_from_counts_challenge/fixture/p_bad.md +6 -0
- package/skills/self-review/scripts/check_reported_p_from_counts_challenge/fixture/p_ok.md +6 -0
- package/skills/self-review/scripts/check_reported_p_from_counts_challenge/problem.md +29 -0
- package/skills/self-review/scripts/check_reported_p_from_counts_challenge/verify.sh +20 -0
- package/skills/self-review/scripts/check_table_percentages.py +211 -0
- package/skills/self-review/scripts/check_table_percentages_challenge/expected/bad.txt +6 -0
- package/skills/self-review/scripts/check_table_percentages_challenge/expected/ok.txt +3 -0
- package/skills/self-review/scripts/check_table_percentages_challenge/fixture/table_bad.md +6 -0
- package/skills/self-review/scripts/check_table_percentages_challenge/fixture/table_ok.md +11 -0
- package/skills/self-review/scripts/check_table_percentages_challenge/problem.md +34 -0
- package/skills/self-review/scripts/check_table_percentages_challenge/verify.sh +40 -0
- package/skills/self-review/skill.yml +11 -0
package/README.md
CHANGED
|
@@ -288,6 +288,8 @@ The E2E pipeline (`orchestrate --e2e`) produces everything up to `qc/`. The `sub
|
|
|
288
288
|
|
|
289
289
|
## What's New
|
|
290
290
|
|
|
291
|
+
**v5.20** — reviewer-arithmetic gates. Five deterministic `self-review` detectors promoted from a peer-review cycle, each recomputing what a manuscript already prints: `check_table_percentages` (an `n (%)` cell vs its column denominator), `check_nested_group_comparison` (a P value comparing an analysed subset against the parent cohort that **contains** it — nested, invalid), `check_reported_p_from_counts` (rebuilds each 2×2 row and recomputes Fisher / Pearson χ² ± Yates in **pure stdlib**, calibrating the family on rows that reproduce), `check_dta_denominators` (sensitivity/specificity denominators vs the reference-standard category counts, behind a matching grand total), and `check_paired_difference_estimator` (an odd-n integer-scale median cannot be non-integer; a zero-width CI; an unnamed estimator). Plus `/peer-review` request-type discipline (disclosure vs computation) and impossibility-claim verification. Additive and backward-compatible; **55 skills / 46 guidelines / 57 integrity detectors / 22 domain-probe modules**. (See the [CHANGELOG](CHANGELOG.md) for v5.0–v5.19.)
|
|
292
|
+
|
|
291
293
|
**v5.19** — reviewer-safety + reporting-checklist batch. A **PDF hidden-text / prompt-injection guard** for `/peer-review` — a PyMuPDF extractor plus a stdlib detector that catch a review-steering instruction hidden in a submitted PDF (white-on-white text, sub-visible fonts, off-page glyphs, invisible render mode, or a document-metadata field) before an LLM ingesting the text layer can be steered by it, and emit visible-only text to feed a model instead of the raw PDF; plus the **TARGET** (target-trial emulation; Cashin et al., *JAMA* 2025) and **REMARK** (prognostic tumour-marker; McShane et al.) reporting checklists. Additive and backward-compatible; **55 skills / 46 guidelines / 52 integrity detectors / 22 domain-probe modules**. (See the [CHANGELOG](CHANGELOG.md) for v5.0–v5.18.)
|
|
292
294
|
|
|
293
295
|
**v5.18** — reliability & workflow-integrity batch. A new deterministic **response-claim gate** for `/revise` and `/peer-review` (verifies a response letter's "we added / we now cite X" against the *revised manuscript body* — a claimed-but-absent edit is caught before it reaches a reviewer; **detectors 50 → 51**), a **reframe / headline-change survivor scan** (`--retired-term` / `--old-value`, finds stale framing or superseded values left in the body, supplement, and figure legends after a reframe), a pre-drafting **backbone full-text readiness gate** for `/write-paper` (the backbone article is *used*, not merely detected), a **skill-registry consistency validator** (`capabilities.yml` ⇄ `skill.yml`, CI-enforced), AI-tool **citation-framing** guidance for `/academic-aio`, and **Demo 4** (PneumoniaMNIST CNN, the model-engineering lane end to end). Additive and backward-compatible; **55 skills / 46 guidelines / 51 integrity detectors / 22 domain-probe modules**. (See the [CHANGELOG](CHANGELOG.md) for v5.0–v5.17.)
|
|
@@ -3048,8 +3048,8 @@
|
|
|
3048
3048
|
},
|
|
3049
3049
|
{
|
|
3050
3050
|
"path": "skills/peer-review/SKILL.md",
|
|
3051
|
-
"size":
|
|
3052
|
-
"sha256": "
|
|
3051
|
+
"size": 75710,
|
|
3052
|
+
"sha256": "484a289ceb990a3301fc3a8093c1e5fae2e8fb00e267d774e4d7df4e0591a748"
|
|
3053
3053
|
},
|
|
3054
3054
|
{
|
|
3055
3055
|
"path": "skills/peer-review/references/aczel_2021_reviewer2_patterns.md",
|
|
@@ -3073,8 +3073,8 @@
|
|
|
3073
3073
|
},
|
|
3074
3074
|
{
|
|
3075
3075
|
"path": "skills/peer-review/references/domain-probes/diagnostic_accuracy.md",
|
|
3076
|
-
"size":
|
|
3077
|
-
"sha256": "
|
|
3076
|
+
"size": 12675,
|
|
3077
|
+
"sha256": "06f8a8e6fcfaf00f25d04e98b58ddf3344ddc1d772057c401a83ef71d012be90"
|
|
3078
3078
|
},
|
|
3079
3079
|
{
|
|
3080
3080
|
"path": "skills/peer-review/references/domain-probes/equity_fairness.md",
|
|
@@ -3118,8 +3118,8 @@
|
|
|
3118
3118
|
},
|
|
3119
3119
|
{
|
|
3120
3120
|
"path": "skills/peer-review/references/domain-probes/observational_confounding.md",
|
|
3121
|
-
"size":
|
|
3122
|
-
"sha256": "
|
|
3121
|
+
"size": 35839,
|
|
3122
|
+
"sha256": "32343fb2ed0d20cb76593c8d07842d0628ce363a7c24a6739f10fa29b37e6398"
|
|
3123
3123
|
},
|
|
3124
3124
|
{
|
|
3125
3125
|
"path": "skills/peer-review/references/domain-probes/polygenic_risk_score.md",
|
|
@@ -3778,8 +3778,8 @@
|
|
|
3778
3778
|
},
|
|
3779
3779
|
{
|
|
3780
3780
|
"path": "skills/self-review/references/domain-probes/diagnostic_accuracy.md",
|
|
3781
|
-
"size":
|
|
3782
|
-
"sha256": "
|
|
3781
|
+
"size": 12675,
|
|
3782
|
+
"sha256": "06f8a8e6fcfaf00f25d04e98b58ddf3344ddc1d772057c401a83ef71d012be90"
|
|
3783
3783
|
},
|
|
3784
3784
|
{
|
|
3785
3785
|
"path": "skills/self-review/references/domain-probes/equity_fairness.md",
|
|
@@ -3823,8 +3823,8 @@
|
|
|
3823
3823
|
},
|
|
3824
3824
|
{
|
|
3825
3825
|
"path": "skills/self-review/references/domain-probes/observational_confounding.md",
|
|
3826
|
-
"size":
|
|
3827
|
-
"sha256": "
|
|
3826
|
+
"size": 35839,
|
|
3827
|
+
"sha256": "32343fb2ed0d20cb76593c8d07842d0628ce363a7c24a6739f10fa29b37e6398"
|
|
3828
3828
|
},
|
|
3829
3829
|
{
|
|
3830
3830
|
"path": "skills/self-review/references/domain-probes/polygenic_risk_score.md",
|
|
@@ -3961,6 +3961,41 @@
|
|
|
3961
3961
|
"size": 6349,
|
|
3962
3962
|
"sha256": "431c5f14ad2c4f59e85b2a5e86150ac708d476e8f2f1da859d81953f522b2117"
|
|
3963
3963
|
},
|
|
3964
|
+
{
|
|
3965
|
+
"path": "skills/self-review/scripts/check_dta_denominators.py",
|
|
3966
|
+
"size": 7798,
|
|
3967
|
+
"sha256": "98c116aa7c42f045565e6e8a24df2b15751aef4a5e7e012ec7e5b5ec17a73850"
|
|
3968
|
+
},
|
|
3969
|
+
{
|
|
3970
|
+
"path": "skills/self-review/scripts/check_dta_denominators_challenge/expected/bad.txt",
|
|
3971
|
+
"size": 506,
|
|
3972
|
+
"sha256": "c784ee7acd5521c7789b6893f4b86deb5c9401e1b1195dc1b77be088af2dda96"
|
|
3973
|
+
},
|
|
3974
|
+
{
|
|
3975
|
+
"path": "skills/self-review/scripts/check_dta_denominators_challenge/expected/ok.txt",
|
|
3976
|
+
"size": 119,
|
|
3977
|
+
"sha256": "875e7cfd99ff0c4e2daa1270f9a5dce30597cc2e58e50c085b7c30d843d57fda"
|
|
3978
|
+
},
|
|
3979
|
+
{
|
|
3980
|
+
"path": "skills/self-review/scripts/check_dta_denominators_challenge/fixture/dta_bad.md",
|
|
3981
|
+
"size": 281,
|
|
3982
|
+
"sha256": "49ed72428ff7cbcc16470fad4f7e63ef20c529fff6a0866d26003cc6cb3f75c7"
|
|
3983
|
+
},
|
|
3984
|
+
{
|
|
3985
|
+
"path": "skills/self-review/scripts/check_dta_denominators_challenge/fixture/dta_ok.md",
|
|
3986
|
+
"size": 280,
|
|
3987
|
+
"sha256": "3ed363e1a00af0db022c522254788d76c3618becff8479b91e98fab52fa77760"
|
|
3988
|
+
},
|
|
3989
|
+
{
|
|
3990
|
+
"path": "skills/self-review/scripts/check_dta_denominators_challenge/problem.md",
|
|
3991
|
+
"size": 1584,
|
|
3992
|
+
"sha256": "fcb419b743fa08256f3098c9a96ee7277819e8b769732d7f4763053535af61a5"
|
|
3993
|
+
},
|
|
3994
|
+
{
|
|
3995
|
+
"path": "skills/self-review/scripts/check_dta_denominators_challenge/verify.sh",
|
|
3996
|
+
"size": 1410,
|
|
3997
|
+
"sha256": "3d6582b582ab5fef657dbb1e8b75a5d478669ccd88fff52e7ebe644ce5ba51f4"
|
|
3998
|
+
},
|
|
3964
3999
|
{
|
|
3965
4000
|
"path": "skills/self-review/scripts/check_editorial_impression.py",
|
|
3966
4001
|
"size": 22032,
|
|
@@ -3976,11 +4011,81 @@
|
|
|
3976
4011
|
"size": 5298,
|
|
3977
4012
|
"sha256": "342bc1126a420111da9c0fe38c6cd68cc45542b0e485202689d7219de9d80712"
|
|
3978
4013
|
},
|
|
4014
|
+
{
|
|
4015
|
+
"path": "skills/self-review/scripts/check_nested_group_comparison.py",
|
|
4016
|
+
"size": 6351,
|
|
4017
|
+
"sha256": "44a7d7ce9913dcf33386de3fae510bc2f1e388c59d338ae482028920b327b37a"
|
|
4018
|
+
},
|
|
4019
|
+
{
|
|
4020
|
+
"path": "skills/self-review/scripts/check_nested_group_comparison_challenge/expected/clean.txt",
|
|
4021
|
+
"size": 104,
|
|
4022
|
+
"sha256": "de4af34352139fba95d091bd65da9c34ffba15d205c1f5ebe53bf1eb23dbe01d"
|
|
4023
|
+
},
|
|
4024
|
+
{
|
|
4025
|
+
"path": "skills/self-review/scripts/check_nested_group_comparison_challenge/expected/nested.txt",
|
|
4026
|
+
"size": 301,
|
|
4027
|
+
"sha256": "497f6f5ce6604a8cce134a165d72768d5bb724458912f926a5cbfbbb22243671"
|
|
4028
|
+
},
|
|
4029
|
+
{
|
|
4030
|
+
"path": "skills/self-review/scripts/check_nested_group_comparison_challenge/fixture/clean.md",
|
|
4031
|
+
"size": 328,
|
|
4032
|
+
"sha256": "882880e171b845160d2f228a45d74fbab55ad9c65aa5ef0f19fc53ff5f4dbb92"
|
|
4033
|
+
},
|
|
4034
|
+
{
|
|
4035
|
+
"path": "skills/self-review/scripts/check_nested_group_comparison_challenge/fixture/nested.md",
|
|
4036
|
+
"size": 243,
|
|
4037
|
+
"sha256": "40d41141e8c60a8338f8e33ac2a597ec2ee4f42fa92ce3cc94cee7be20f52b2d"
|
|
4038
|
+
},
|
|
4039
|
+
{
|
|
4040
|
+
"path": "skills/self-review/scripts/check_nested_group_comparison_challenge/problem.md",
|
|
4041
|
+
"size": 1494,
|
|
4042
|
+
"sha256": "f02cff50c0c86f28ea144cb06b190420d9a02f2217d15c271157943b3f6a4984"
|
|
4043
|
+
},
|
|
4044
|
+
{
|
|
4045
|
+
"path": "skills/self-review/scripts/check_nested_group_comparison_challenge/verify.sh",
|
|
4046
|
+
"size": 1478,
|
|
4047
|
+
"sha256": "c84ba913c3ba0f4d9ecc9bf84a77836513314c967491c2bb0d8666b1baf0281e"
|
|
4048
|
+
},
|
|
3979
4049
|
{
|
|
3980
4050
|
"path": "skills/self-review/scripts/check_null_calibration.py",
|
|
3981
4051
|
"size": 8585,
|
|
3982
4052
|
"sha256": "e5bd71c515554b3acbaefd3807e584364a536dc6e1101bec5b524bc26b341309"
|
|
3983
4053
|
},
|
|
4054
|
+
{
|
|
4055
|
+
"path": "skills/self-review/scripts/check_paired_difference_estimator.py",
|
|
4056
|
+
"size": 6612,
|
|
4057
|
+
"sha256": "e4a4c4c59b7dc50a8eeace2a9dfa7ba988120b4ede5f307d9836c37b40a2b76f"
|
|
4058
|
+
},
|
|
4059
|
+
{
|
|
4060
|
+
"path": "skills/self-review/scripts/check_paired_difference_estimator_challenge/expected/bad.txt",
|
|
4061
|
+
"size": 550,
|
|
4062
|
+
"sha256": "3f9204146334e1f18c67997cb208d1e3702e9f2c7ed865f31f342c03ca962935"
|
|
4063
|
+
},
|
|
4064
|
+
{
|
|
4065
|
+
"path": "skills/self-review/scripts/check_paired_difference_estimator_challenge/expected/ok.txt",
|
|
4066
|
+
"size": 111,
|
|
4067
|
+
"sha256": "4706dc20a9e9391a032acf5b91ce71b6ec619174ffc7e676eee4fc2cf91d4e40"
|
|
4068
|
+
},
|
|
4069
|
+
{
|
|
4070
|
+
"path": "skills/self-review/scripts/check_paired_difference_estimator_challenge/fixture/paired_bad.md",
|
|
4071
|
+
"size": 257,
|
|
4072
|
+
"sha256": "a268c335cc776f26f6b366044d40d75538fba87e2730c58cc8cbeb66780f44eb"
|
|
4073
|
+
},
|
|
4074
|
+
{
|
|
4075
|
+
"path": "skills/self-review/scripts/check_paired_difference_estimator_challenge/fixture/paired_ok.md",
|
|
4076
|
+
"size": 287,
|
|
4077
|
+
"sha256": "0be43e775f0e6d45162c9d0dd23e591b916992f2abd041ff71483920cb4d8a90"
|
|
4078
|
+
},
|
|
4079
|
+
{
|
|
4080
|
+
"path": "skills/self-review/scripts/check_paired_difference_estimator_challenge/problem.md",
|
|
4081
|
+
"size": 1505,
|
|
4082
|
+
"sha256": "ede7ecbef797e18958abd037d3534f11b71e1027f87a3a99c310a24484da69ed"
|
|
4083
|
+
},
|
|
4084
|
+
{
|
|
4085
|
+
"path": "skills/self-review/scripts/check_paired_difference_estimator_challenge/verify.sh",
|
|
4086
|
+
"size": 1531,
|
|
4087
|
+
"sha256": "b3172bbdc15868b8d4f16534e1c0049ec8b694ac864f22a989ee00aa84d218e4"
|
|
4088
|
+
},
|
|
3984
4089
|
{
|
|
3985
4090
|
"path": "skills/self-review/scripts/check_panel_diversity.py",
|
|
3986
4091
|
"size": 15324,
|
|
@@ -3996,6 +4101,41 @@
|
|
|
3996
4101
|
"size": 17564,
|
|
3997
4102
|
"sha256": "007100ed2ec23d01a9352feae10c5af23e7b3e88d91bb7fda7c3c899772aaa85"
|
|
3998
4103
|
},
|
|
4104
|
+
{
|
|
4105
|
+
"path": "skills/self-review/scripts/check_reported_p_from_counts.py",
|
|
4106
|
+
"size": 8813,
|
|
4107
|
+
"sha256": "86771e8dfbad9d9680fa7cfd15cc98b63a247a318cd86c4162d852359c82a252"
|
|
4108
|
+
},
|
|
4109
|
+
{
|
|
4110
|
+
"path": "skills/self-review/scripts/check_reported_p_from_counts_challenge/expected/bad.txt",
|
|
4111
|
+
"size": 308,
|
|
4112
|
+
"sha256": "b9d0da79b8eedda2c8a17ef6852400e6b7adb5e556d561cf544c8a5e5a8fb875"
|
|
4113
|
+
},
|
|
4114
|
+
{
|
|
4115
|
+
"path": "skills/self-review/scripts/check_reported_p_from_counts_challenge/expected/ok.txt",
|
|
4116
|
+
"size": 112,
|
|
4117
|
+
"sha256": "cddb1acfec3c0a27e18c24755373a0bcb5521eae3d6ad6e97b06a03eabfafdbc"
|
|
4118
|
+
},
|
|
4119
|
+
{
|
|
4120
|
+
"path": "skills/self-review/scripts/check_reported_p_from_counts_challenge/fixture/p_bad.md",
|
|
4121
|
+
"size": 206,
|
|
4122
|
+
"sha256": "b22b3cae304a8986a9b9ec49377601dc96f791fbbc0e04cd8adaf7c9952707dc"
|
|
4123
|
+
},
|
|
4124
|
+
{
|
|
4125
|
+
"path": "skills/self-review/scripts/check_reported_p_from_counts_challenge/fixture/p_ok.md",
|
|
4126
|
+
"size": 205,
|
|
4127
|
+
"sha256": "15546bf0a9be157265dd3ec80be46d32b7fbe5386ad6e31ea6c16ea669abf3ac"
|
|
4128
|
+
},
|
|
4129
|
+
{
|
|
4130
|
+
"path": "skills/self-review/scripts/check_reported_p_from_counts_challenge/problem.md",
|
|
4131
|
+
"size": 1572,
|
|
4132
|
+
"sha256": "e2df6d50acbce0da65f1af6f767a63e7007a855f40e5ef7b549d8348bd830133"
|
|
4133
|
+
},
|
|
4134
|
+
{
|
|
4135
|
+
"path": "skills/self-review/scripts/check_reported_p_from_counts_challenge/verify.sh",
|
|
4136
|
+
"size": 1509,
|
|
4137
|
+
"sha256": "1a1fddcdef6d23a3c6e2f83c924180c2703355953223d5d82cd89e01615e436d"
|
|
4138
|
+
},
|
|
3999
4139
|
{
|
|
4000
4140
|
"path": "skills/self-review/scripts/check_reviewer_team_consistency.py",
|
|
4001
4141
|
"size": 15481,
|
|
@@ -4016,10 +4156,45 @@
|
|
|
4016
4156
|
"size": 13579,
|
|
4017
4157
|
"sha256": "c61bd5e485d2bce4a6efd7bf2a7980aa0d31ebecb876a7c6895fd12cb6bf7f1c"
|
|
4018
4158
|
},
|
|
4159
|
+
{
|
|
4160
|
+
"path": "skills/self-review/scripts/check_table_percentages.py",
|
|
4161
|
+
"size": 8674,
|
|
4162
|
+
"sha256": "102cc795660fd41c40219b006dc657edd8d77f9195cf5b8fd2e5e470d3c28f3a"
|
|
4163
|
+
},
|
|
4164
|
+
{
|
|
4165
|
+
"path": "skills/self-review/scripts/check_table_percentages_challenge/expected/bad.txt",
|
|
4166
|
+
"size": 301,
|
|
4167
|
+
"sha256": "6fed046d75f3a02818e0a3ac4aa0ad907b17eab7e05e9629467e1d54bea51856"
|
|
4168
|
+
},
|
|
4169
|
+
{
|
|
4170
|
+
"path": "skills/self-review/scripts/check_table_percentages_challenge/expected/ok.txt",
|
|
4171
|
+
"size": 112,
|
|
4172
|
+
"sha256": "2fb34506bf65909fdb040acf085c4c18810b52497b6b1a53a2e08509a4465fd8"
|
|
4173
|
+
},
|
|
4174
|
+
{
|
|
4175
|
+
"path": "skills/self-review/scripts/check_table_percentages_challenge/fixture/table_bad.md",
|
|
4176
|
+
"size": 121,
|
|
4177
|
+
"sha256": "841bcb7d75c8e51512c59de171d82c9959b72976eef29f631ba9440c873357d6"
|
|
4178
|
+
},
|
|
4179
|
+
{
|
|
4180
|
+
"path": "skills/self-review/scripts/check_table_percentages_challenge/fixture/table_ok.md",
|
|
4181
|
+
"size": 235,
|
|
4182
|
+
"sha256": "ea5999c08b15a928606ca154c504545ed5804e45bade45ffca4d12b5849cc0bc"
|
|
4183
|
+
},
|
|
4184
|
+
{
|
|
4185
|
+
"path": "skills/self-review/scripts/check_table_percentages_challenge/problem.md",
|
|
4186
|
+
"size": 2072,
|
|
4187
|
+
"sha256": "0af0e84599a8ac52a33ce33bc3244803ccc34d6f8f7f8de24234ac3b64a9a61a"
|
|
4188
|
+
},
|
|
4189
|
+
{
|
|
4190
|
+
"path": "skills/self-review/scripts/check_table_percentages_challenge/verify.sh",
|
|
4191
|
+
"size": 2041,
|
|
4192
|
+
"sha256": "b4c9eb16f291841c1d6dc74a5554a49b5dc60d3562d0e328e01a78797f8d32ad"
|
|
4193
|
+
},
|
|
4019
4194
|
{
|
|
4020
4195
|
"path": "skills/self-review/skill.yml",
|
|
4021
|
-
"size":
|
|
4022
|
-
"sha256": "
|
|
4196
|
+
"size": 3278,
|
|
4197
|
+
"sha256": "84ea9430003ffc8f1b2e009600ac3b4614abb84a697ec1891a39086aa65fe404"
|
|
4023
4198
|
},
|
|
4024
4199
|
{
|
|
4025
4200
|
"path": "skills/setup-medsci/SKILL.md",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "medsci-skills",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.20.0",
|
|
4
4
|
"description": "MedSci Skills — a medical/scientific research skill suite for AI coding agents (Claude Code, Codex, Cursor, Copilot). The npm package is a terminal-friendly installer shortcut; the canonical distribution remains the GitHub repository and the Claude Code plugin marketplace.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://github.com/Aperivue/medsci-skills#readme",
|
|
@@ -368,6 +368,13 @@ calibration, optimistic validation reporting, selective outcome reporting). Each
|
|
|
368
368
|
it as a partner (Aczel-compliant), and calibrate severity (design-level → Major #1). Model
|
|
369
369
|
the anchoring and phrasing; do not copy — they are synthetic teaching examples.
|
|
370
370
|
|
|
371
|
+
**Request-type discipline (classify every Major's ask before it ships).** Sort each request into two kinds:
|
|
372
|
+
|
|
373
|
+
- **Disclosure** — the study already holds the answer and has not printed it (the analysis unit; the subset's characteristics; a CI already computed; whether the model was trained on this cohort; the reading order). It costs the authors nothing to produce and *surfaces* errors; the highest-value comments are almost always this kind, including one that forces an over-claiming title to be softened.
|
|
374
|
+
- **Computation** — the authors must produce a number that does not yet exist (test this difference; bootstrap a CI; give an effect size). It creates a **new, unreviewed error surface** produced under revision deadline by authors who will not re-check it and accepted next round by a reviewer who reads its existence as compliance.
|
|
375
|
+
|
|
376
|
+
A computation request must carry an explicit justification that the existing tables cannot answer the question; otherwise reword it as disclosure or drop it. Prefer **naming the estimator** you want (e.g. *Hodges–Lehmann pseudomedian*) over a loose phrase (*"paired median differences"*), which authors adopt verbatim (an odd-n integer-scale "median difference" is impossible — `check_paired_difference_estimator.py`). A comment may be **both** — split it: never *request* a subset-vs-parent-cohort P value, because the groups are nested and the test is invalid (`check_nested_group_comparison.py`, and the observational/DTA domain probes); ask for the subset's characteristics (disclosure) and judge representativeness by magnitude. This is not "ask for less" — a short review with two computation requests is worse than a long one with ten disclosure requests.
|
|
377
|
+
|
|
371
378
|
Generate `{manuscript_id}_review_draft.md`:
|
|
372
379
|
|
|
373
380
|
```markdown
|
|
@@ -468,7 +475,7 @@ After drafting, verify mechanically:
|
|
|
468
475
|
Comments contain no Reject-grade language (including value-judgment deferral to the board) left
|
|
469
476
|
inconsistent with a softer recommendation.
|
|
470
477
|
13. **Observational-confounding QC** (if Phase 2E applied): For any covariate imbalanced by exposure in Table 1 but absent from the adjustment set (O1), confirm the comment requests a concrete extended-adjustment sensitivity model, not a vague "adjust for more confounders." Confirm a selection/collider structure (O3) or an undisclosed complete-case collapse from a structural-zero dose covariate (O5) is raised at design-level severity, and that any E-value request (O6) targets the declared primary estimate rather than a supporting one.
|
|
471
|
-
14. **Verify-your-own-criticism** (all reviews): For each Major framed as a technical inaccuracy or a citation–claim mismatch, confirm the reviewer's own assertion was checked against a current authoritative source (full paper, CrossRef, arXiv). Downgrade unverified technical claims to a hedged "Please verify…"; keep confirmed ones firm. Watch for status drift (a "preprint" since published; a method since adapted) before asserting the manuscript is wrong.
|
|
478
|
+
14. **Verify-your-own-criticism** (all reviews): For each Major framed as a technical inaccuracy or a citation–claim mismatch, confirm the reviewer's own assertion was checked against a current authoritative source (full paper, CrossRef, arXiv). Downgrade unverified technical claims to a hedged "Please verify…"; keep confirmed ones firm. Watch for status drift (a "preprint" since published; a method since adapted) before asserting the manuscript is wrong. **This extends to any assertion of arithmetic or statistical *impossibility* derived from the manuscript's own summary statistics** — the highest-embarrassment class, asserted with certainty and trivially falsified by the authors. Any claim containing *requires / cannot / impossible / must / contradicts* must be restated as an explicit premise→conclusion pair and stress-tested for a counterexample before submission. Two recurring traps: **a quantile or IQR statement does not constrain the tail**, and **an agreement coefficient (κ, ICC) does not constrain the marginal distribution.** Separately, for a **reviewer-requested** new statistic that appears Resolved at revision, re-derive it from the manuscript's own cells before accepting — its existence is not evidence of its correctness, and a requested analysis is the highest-prior-probability location for error in the revision (`~/.claude/rules/peer-review-response-verification.md`).
|
|
472
479
|
15. **Image-synthesis QC** (if Phase 2K applied): Confirm the determinism/information-ceiling point (IS1) is raised whenever the manuscript reads a same-reader source→source+synthetic gain as added diagnostic information without a source→label baseline, that undescribed slice/mask provenance (IS2) is surfaced as a leakage/circularity concern rather than a reporting nicety, that quantitative agreement is checked at the lesion/target level not only globally (IS3), and that a biological-information claim built on image similarity alone is tempered (IS4). Per Phase 2F, confirm IS2/IS4 were treated as unfixable-in-current-form when present.
|
|
473
480
|
16. **Reference-integrity QC** (all original-research reviews): Confirm the load-bearing Introduction/Discussion citations (those used as evidence the method or premise works) were spot-checked — a cited paper doing a different task, a duplicate reference, or a wrong year/author is a Minor (Major if the premise rests on it), and any unconfirmed suspicion is phrased "please verify" rather than asserted.
|
|
474
481
|
|
|
@@ -494,6 +501,8 @@ Fix all issues found, then present to user.
|
|
|
494
501
|
- [ ] Reference-baseline ratio (`wc / 545w`) reported; ratio > 2.0 trimmed
|
|
495
502
|
- [ ] Hard cap 1400 words not exceeded
|
|
496
503
|
- [ ] AI pattern density within thresholds (em-dash ≤2/1000w; structural rule-of-three ≤2/Major; significance inflation 0/Major; hedged Minor ≥50%)
|
|
504
|
+
- [ ] Every Major's ask classified disclosure vs computation; each computation request justified (existing tables cannot answer it) and its estimator named; no subset-vs-parent-cohort P value requested
|
|
505
|
+
- [ ] Impossibility claims (requires/cannot/impossible/must/contradicts) restated as premise→conclusion + counterexample-tested; reviewer-requested new statistics re-derived from the manuscript's own cells (correctness ≠ presence)
|
|
497
506
|
- [ ] Fatal flaw hierarchy stated in Confidential Comments (if applicable)
|
|
498
507
|
- [ ] Reject recommendations (if used): §1C condition checklist (design-level flaw + speculative practical value 3-trigger + novelty gap) explicitly verified — at least 2 of 3 conditions met
|
|
499
508
|
- [ ] AI/method/review Major Revision (or Reconsider) recommendations: Phase 2F contribution/value gate checked; weak novelty + weak utility not silently softened; for review articles, weak-novelty/no-distinct-contribution treated as unfixable-in-current-form (escalate toward Reject); unfixable defects govern tier over fixable list; confidential note carries no Reject-grade language left inconsistent with a softer recommendation
|
|
@@ -17,6 +17,7 @@ A checklist for **diagnostic test accuracy (DTA) primary studies** — an index
|
|
|
17
17
|
**D2 — Spectrum + sampling design**:
|
|
18
18
|
- How were participants sampled — a **single-gate** consecutive/random series of those with the target condition suspected (preserves spectrum and prevalence), or a **two-gate** case-control design (known cases vs healthy controls)? Two-gate sampling **overestimates** accuracy and distorts spectrum; it is a MAJOR interpretive limit when the headline is clinical accuracy, not just proof-of-concept.
|
|
19
19
|
- Is the study **prospective** with a pre-specified index threshold, or retrospective with a threshold chosen on the same data (D-linked to optimism)? Spectrum (disease severity, comorbid mimics) should match the intended-use population.
|
|
20
|
+
- **Representativeness-of-the-analysed-subset table**: when the analysed subset (e.g. the pathology-correlated cases) is compared with the parent cohort that *contains* it, a reported P value is **invalid** — nested groups, no valid null (`check_nested_group_comparison.py`). Judge representativeness by magnitude; the valid contrast is subset vs remainder. Separately, the sensitivity a/(a+c) and specificity d/(b+d) **denominators must equal the reference-standard positive / negative counts** in the characteristics table — grand-total agreement does **not** license a per-category split mismatch, which is exactly what lets it survive review under an Abstract headline (`check_dta_denominators.py`).
|
|
20
21
|
|
|
21
22
|
**D3 — Blinding of index and reference interpretation**:
|
|
22
23
|
- Was the **index test** interpreted blind to the reference standard and to clinical information that would not be available at the point of use, and vice versa? Unblinded interpretation (review bias) inflates agreement.
|
|
@@ -67,6 +67,7 @@ A 17-probe checklist for observational studies (cohort, case-control, cross-sect
|
|
|
67
67
|
- When a manuscript reports an effect-size **gradient or trend across nested / overlapping cohorts or subsets** — a "comparator-refinement gradient" where the exposed arm is held fixed and only the reference group is progressively refined (Frame A ⊃ B ⊃ C; e.g. 144 → 119 → 84 events as competing-cause cases are removed from the reference) — the across-frame shift is **attributable by construction**: the reference changed, not the exposure. The estimates are not independent and there is no contrast being tested.
|
|
68
68
|
- Inferential language for that gradient — "the association **attenuated significantly** across frames", "the [confounder] **largely accounted for** the effect", "a **stepwise reduction**" — is unsupported without a formal **difference / interaction test**. Two acceptable resolutions: (a) keep the gradient **descriptive** (report each frame's estimate and state that the shift follows by construction from refining the reference, not from a tested difference), or (b) fit **one model** with a frame-membership × exposure interaction (or a formally nested-model comparison) and report that interaction term.
|
|
69
69
|
- This is the cohort comparator-refinement analogue of the sensitivity-envelope rule in `~/.claude/rules/scope-coherence-gate.md` (a numeric analysis introduced as if estimable/tested without the supporting test). An inferential "attenuated/accounted-for/reduced across overlapping subsets" claim with no difference/interaction test → MAJOR; downgrade to descriptive or add the interaction model.
|
|
70
|
+
- **A P value comparing an analysed subset with the parent cohort that contains it is invalid, not merely uninformative** — the groups are nested (the subset's patients are *inside* the "full cohort"), so the two-group test has no valid null. This is the RC5 baseline-testing failure's observational sibling, and here it is worse than uninformative. The shape appears when authors answer a selection-bias comment with a "representativeness of the analysed subset" table (full cohort n vs subset n, with P values). Judge representativeness by the **magnitude** of the difference; if a test is genuinely wanted, the valid contrast is **subset vs remainder** (n_full − n_subset), which is disjoint. Flag any subset-vs-parent P as MAJOR (the reported P is uninterpretable). The self-review sibling automates this as a deterministic gate (`scripts/check_nested_group_comparison.py`); on the reviewer side, do not *request* such a table either — ask for the subset's characteristics (disclosure) and read representativeness off the magnitudes.
|
|
70
71
|
|
|
71
72
|
**O11 — Complex-survey design & weighting (NHANES / KNHANES / CHNS and other multistage probability samples)**:
|
|
72
73
|
- When the data are a **complex probability survey** (NHANES, KNHANES, CHNS, BRFSS, national health-examination surveys), unweighted or naively-weighted analysis is wrong. Check that the analysis is **design-based**: the **sampling weight** is applied AND the **stratification** (e.g. NHANES `SDMVSTRA`) and **primary sampling unit / cluster** (`SDMVPSU`) are declared so that variances use Taylor linearization or replicate weights — a weighted point estimate with **model-based (unclustered) standard errors** still understates uncertainty.
|
|
@@ -17,6 +17,7 @@ A checklist for **diagnostic test accuracy (DTA) primary studies** — an index
|
|
|
17
17
|
**D2 — Spectrum + sampling design**:
|
|
18
18
|
- How were participants sampled — a **single-gate** consecutive/random series of those with the target condition suspected (preserves spectrum and prevalence), or a **two-gate** case-control design (known cases vs healthy controls)? Two-gate sampling **overestimates** accuracy and distorts spectrum; it is a MAJOR interpretive limit when the headline is clinical accuracy, not just proof-of-concept.
|
|
19
19
|
- Is the study **prospective** with a pre-specified index threshold, or retrospective with a threshold chosen on the same data (D-linked to optimism)? Spectrum (disease severity, comorbid mimics) should match the intended-use population.
|
|
20
|
+
- **Representativeness-of-the-analysed-subset table**: when the analysed subset (e.g. the pathology-correlated cases) is compared with the parent cohort that *contains* it, a reported P value is **invalid** — nested groups, no valid null (`check_nested_group_comparison.py`). Judge representativeness by magnitude; the valid contrast is subset vs remainder. Separately, the sensitivity a/(a+c) and specificity d/(b+d) **denominators must equal the reference-standard positive / negative counts** in the characteristics table — grand-total agreement does **not** license a per-category split mismatch, which is exactly what lets it survive review under an Abstract headline (`check_dta_denominators.py`).
|
|
20
21
|
|
|
21
22
|
**D3 — Blinding of index and reference interpretation**:
|
|
22
23
|
- Was the **index test** interpreted blind to the reference standard and to clinical information that would not be available at the point of use, and vice versa? Unblinded interpretation (review bias) inflates agreement.
|
|
@@ -67,6 +67,7 @@ A 17-probe checklist for observational studies (cohort, case-control, cross-sect
|
|
|
67
67
|
- When a manuscript reports an effect-size **gradient or trend across nested / overlapping cohorts or subsets** — a "comparator-refinement gradient" where the exposed arm is held fixed and only the reference group is progressively refined (Frame A ⊃ B ⊃ C; e.g. 144 → 119 → 84 events as competing-cause cases are removed from the reference) — the across-frame shift is **attributable by construction**: the reference changed, not the exposure. The estimates are not independent and there is no contrast being tested.
|
|
68
68
|
- Inferential language for that gradient — "the association **attenuated significantly** across frames", "the [confounder] **largely accounted for** the effect", "a **stepwise reduction**" — is unsupported without a formal **difference / interaction test**. Two acceptable resolutions: (a) keep the gradient **descriptive** (report each frame's estimate and state that the shift follows by construction from refining the reference, not from a tested difference), or (b) fit **one model** with a frame-membership × exposure interaction (or a formally nested-model comparison) and report that interaction term.
|
|
69
69
|
- This is the cohort comparator-refinement analogue of the sensitivity-envelope rule in `~/.claude/rules/scope-coherence-gate.md` (a numeric analysis introduced as if estimable/tested without the supporting test). An inferential "attenuated/accounted-for/reduced across overlapping subsets" claim with no difference/interaction test → MAJOR; downgrade to descriptive or add the interaction model.
|
|
70
|
+
- **A P value comparing an analysed subset with the parent cohort that contains it is invalid, not merely uninformative** — the groups are nested (the subset's patients are *inside* the "full cohort"), so the two-group test has no valid null. This is the RC5 baseline-testing failure's observational sibling, and here it is worse than uninformative. The shape appears when authors answer a selection-bias comment with a "representativeness of the analysed subset" table (full cohort n vs subset n, with P values). Judge representativeness by the **magnitude** of the difference; if a test is genuinely wanted, the valid contrast is **subset vs remainder** (n_full − n_subset), which is disjoint. Flag any subset-vs-parent P as MAJOR (the reported P is uninterpretable). The self-review sibling automates this as a deterministic gate (`scripts/check_nested_group_comparison.py`); on the reviewer side, do not *request* such a table either — ask for the subset's characteristics (disclosure) and read representativeness off the magnitudes.
|
|
70
71
|
|
|
71
72
|
**O11 — Complex-survey design & weighting (NHANES / KNHANES / CHNS and other multistage probability samples)**:
|
|
72
73
|
- When the data are a **complex probability survey** (NHANES, KNHANES, CHNS, BRFSS, national health-examination surveys), unweighted or naively-weighted analysis is wrong. Check that the analysis is **design-based**: the **sampling weight** is applied AND the **stratification** (e.g. NHANES `SDMVSTRA`) and **primary sampling unit / cluster** (`SDMVPSU`) are declared so that variances use Taylor linearization or replicate weights — a weighted point estimate with **model-based (unclustered) standard errors** still understates uncertainty.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""check_dta_denominators.py — cross-check diagnostic-accuracy sensitivity /
|
|
3
|
+
specificity denominators against the reference-standard category counts in the
|
|
4
|
+
characteristics table.
|
|
5
|
+
|
|
6
|
+
Sensitivity is a/(a+c) over the disease-positive patients; specificity is d/(b+d)
|
|
7
|
+
over the disease-negative patients. Those denominators must equal the reference-
|
|
8
|
+
standard positive / negative counts printed in the characteristics table. When
|
|
9
|
+
they disagree while the grand total still matches (e.g. table 14/19, Results
|
|
10
|
+
15/18, both summing to 33), a totals check passes and the split-level error — which
|
|
11
|
+
sits under an Abstract headline — survives multiple review rounds. This detector
|
|
12
|
+
recovers both category counts and both denominators and asserts equality per
|
|
13
|
+
category, refusing to accept grand-total agreement as passing.
|
|
14
|
+
|
|
15
|
+
Also `STAGE_ROWSUM`: a staging-confusion breakdown must satisfy
|
|
16
|
+
correctly-staged + over-staged + under-staged == n.
|
|
17
|
+
|
|
18
|
+
Runs on FIRST submissions, not only revisions — the source incident had both
|
|
19
|
+
numbers on the page from submission one.
|
|
20
|
+
|
|
21
|
+
Stdlib-only. Reads the manuscript, never writes it.
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
python3 check_dta_denominators.py --manuscript paper.md [--strict] [--quiet] [--json]
|
|
25
|
+
Exit: 0 clean; with --strict, 1 on any mismatch; 2 on input/usage error.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import argparse
|
|
30
|
+
import json
|
|
31
|
+
import re
|
|
32
|
+
import sys
|
|
33
|
+
from dataclasses import dataclass, field, asdict
|
|
34
|
+
|
|
35
|
+
SEP_RE = re.compile(r"^\s*\|?[\s:|-]*-[\s:|-]*\|?\s*$")
|
|
36
|
+
NEG_LABEL_RE = re.compile(r"\b(p?N\s*0|node[- ]negative|disease[- ](?:free|negative)|absent|benign|non-?malignant)\b", re.I)
|
|
37
|
+
POS_LABEL_RE = re.compile(r"\b(p?N\s*[1-9]|node[- ]positive|disease[- ]positive|present|malignant|metasta\w+)\b", re.I)
|
|
38
|
+
# "sensitivity ... 13/18" or "13 of 18"; bounded gap so it does not cross a
|
|
39
|
+
# sentence boundary but tolerates a decimal percentage (e.g. "72.2% (13/18)").
|
|
40
|
+
SENS_RE = re.compile(r"sensitivit\w*[^\n]{0,40}?\b(\d+)\s*(?:/|of)\s*(\d+)", re.I)
|
|
41
|
+
SPEC_RE = re.compile(r"specificit\w*[^\n]{0,40}?\b(\d+)\s*(?:/|of)\s*(\d+)", re.I)
|
|
42
|
+
CORRECT_RE = re.compile(r"correctl\w*[^.\n]*?\b(\d+)", re.I)
|
|
43
|
+
OVER_RE = re.compile(r"over[- ]?stag\w*[^.\n]*?\b(\d+)", re.I)
|
|
44
|
+
UNDER_RE = re.compile(r"under[- ]?stag\w*[^.\n]*?\b(\d+)", re.I)
|
|
45
|
+
STAGE_N_RE = re.compile(r"\b(?:in|of|among)\s+(\d+)\s+(?:patients|exams|cases|lesions)\b", re.I)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class Finding:
|
|
50
|
+
kind: str
|
|
51
|
+
severity: str
|
|
52
|
+
detail: str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class Report:
|
|
57
|
+
source: str
|
|
58
|
+
findings: list[Finding] = field(default_factory=list)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def n_flag(self) -> int:
|
|
62
|
+
return sum(1 for f in self.findings if f.severity == "MAJOR")
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def verdict(self) -> str:
|
|
66
|
+
return "DENOMINATOR MISMATCH" if self.n_flag else "OK"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _split_row(line: str) -> list[str]:
|
|
70
|
+
s = line.strip()
|
|
71
|
+
if s.startswith("|"):
|
|
72
|
+
s = s[1:]
|
|
73
|
+
if s.endswith("|"):
|
|
74
|
+
s = s[:-1]
|
|
75
|
+
return [c.strip() for c in s.split("|")]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _reference_counts(text: str):
|
|
79
|
+
"""Return (neg_total, pos_total) summed over reference-standard rows, or None."""
|
|
80
|
+
neg = pos = 0
|
|
81
|
+
seen = False
|
|
82
|
+
lines = text.splitlines()
|
|
83
|
+
i, n = 0, len(lines)
|
|
84
|
+
while i < n:
|
|
85
|
+
if "|" in lines[i] and i + 1 < n and SEP_RE.match(lines[i + 1]) and "-" in lines[i + 1]:
|
|
86
|
+
j = i + 2
|
|
87
|
+
while j < n and "|" in lines[j] and lines[j].strip():
|
|
88
|
+
cells = _split_row(lines[j])
|
|
89
|
+
if cells:
|
|
90
|
+
label = cells[0]
|
|
91
|
+
cm = re.search(r"\b(\d[\d,]*)\b", " ".join(cells[1:]))
|
|
92
|
+
if cm:
|
|
93
|
+
cnt = int(cm.group(1).replace(",", ""))
|
|
94
|
+
if NEG_LABEL_RE.search(label):
|
|
95
|
+
neg += cnt; seen = True
|
|
96
|
+
elif POS_LABEL_RE.search(label):
|
|
97
|
+
pos += cnt; seen = True
|
|
98
|
+
j += 1
|
|
99
|
+
i = j
|
|
100
|
+
else:
|
|
101
|
+
i += 1
|
|
102
|
+
return (neg, pos) if seen else None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def audit(text: str, source: str) -> Report:
|
|
106
|
+
rep = Report(source=source)
|
|
107
|
+
ref = _reference_counts(text)
|
|
108
|
+
sens = SENS_RE.search(text)
|
|
109
|
+
spec = SPEC_RE.search(text)
|
|
110
|
+
|
|
111
|
+
if ref and (sens or spec):
|
|
112
|
+
neg_ct, pos_ct = ref
|
|
113
|
+
sens_denom = int(sens.group(2)) if sens else None
|
|
114
|
+
spec_denom = int(spec.group(2)) if spec else None
|
|
115
|
+
if sens_denom is not None and pos_ct and sens_denom != pos_ct:
|
|
116
|
+
rep.findings.append(Finding(
|
|
117
|
+
"DTA_DENOMINATOR_MISMATCH", "MAJOR",
|
|
118
|
+
f"sensitivity denominator {sens_denom} (disease-positive) ≠ reference-standard "
|
|
119
|
+
f"positive count {pos_ct} from the characteristics table"))
|
|
120
|
+
if spec_denom is not None and neg_ct and spec_denom != neg_ct:
|
|
121
|
+
rep.findings.append(Finding(
|
|
122
|
+
"DTA_DENOMINATOR_MISMATCH", "MAJOR",
|
|
123
|
+
f"specificity denominator {spec_denom} (disease-negative) ≠ reference-standard "
|
|
124
|
+
f"negative count {neg_ct} from the characteristics table"))
|
|
125
|
+
# note when the grand totals still agree — that is what hides it
|
|
126
|
+
if rep.n_flag and sens_denom and spec_denom and neg_ct + pos_ct == sens_denom + spec_denom:
|
|
127
|
+
rep.findings.append(Finding(
|
|
128
|
+
"GRAND_TOTAL_AGREES", "INFO",
|
|
129
|
+
f"grand totals agree ({neg_ct}+{pos_ct} = {sens_denom}+{spec_denom} = "
|
|
130
|
+
f"{neg_ct + pos_ct}), so a totals-only check passes — the split-level "
|
|
131
|
+
f"mismatch is the defect"))
|
|
132
|
+
|
|
133
|
+
c, o, u = CORRECT_RE.search(text), OVER_RE.search(text), UNDER_RE.search(text)
|
|
134
|
+
sn = STAGE_N_RE.search(text)
|
|
135
|
+
if c and o and u and sn:
|
|
136
|
+
s = int(c.group(1)) + int(o.group(1)) + int(u.group(1))
|
|
137
|
+
tot = int(sn.group(1))
|
|
138
|
+
if s != tot:
|
|
139
|
+
rep.findings.append(Finding(
|
|
140
|
+
"STAGE_ROWSUM", "MAJOR",
|
|
141
|
+
f"correctly {c.group(1)} + over {o.group(1)} + under {u.group(1)} = {s} ≠ n={tot}"))
|
|
142
|
+
return rep
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def format_report(rep: Report, color: bool) -> str:
|
|
146
|
+
tag = {"OK": "\033[92m", "DENOMINATOR MISMATCH": "\033[91m"}.get(rep.verdict, "") if color else ""
|
|
147
|
+
end = "\033[0m" if color else ""
|
|
148
|
+
out = [f"{tag}== {rep.verdict} =={end} {rep.source}", f"mismatches={rep.n_flag}"]
|
|
149
|
+
if not rep.findings:
|
|
150
|
+
out.append("sens/spec denominators reconcile with the reference-standard category counts.")
|
|
151
|
+
return "\n".join(out)
|
|
152
|
+
for f in sorted(rep.findings, key=lambda x: (x.severity != "MAJOR", x.kind, x.detail)):
|
|
153
|
+
out.append(f"[{f.severity:<5}] {f.kind} {f.detail}")
|
|
154
|
+
return "\n".join(out)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def main(argv: list[str] | None = None) -> int:
|
|
158
|
+
ap = argparse.ArgumentParser(description=__doc__,
|
|
159
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
160
|
+
ap.add_argument("--manuscript", required=True, help="manuscript markdown/text")
|
|
161
|
+
ap.add_argument("--strict", action="store_true", help="exit 1 on any mismatch")
|
|
162
|
+
ap.add_argument("--quiet", action="store_true", help="suppress the report; exit code only")
|
|
163
|
+
ap.add_argument("--json", action="store_true", help="emit JSON instead of a text report")
|
|
164
|
+
args = ap.parse_args(argv)
|
|
165
|
+
try:
|
|
166
|
+
text = open(args.manuscript, encoding="utf-8").read()
|
|
167
|
+
except OSError as e:
|
|
168
|
+
print(f"error: cannot read manuscript: {e}", file=sys.stderr)
|
|
169
|
+
return 2
|
|
170
|
+
rep = audit(text, args.manuscript)
|
|
171
|
+
if not args.quiet:
|
|
172
|
+
if args.json:
|
|
173
|
+
print(json.dumps({"source": rep.source, "verdict": rep.verdict,
|
|
174
|
+
"findings": [asdict(f) for f in rep.findings]},
|
|
175
|
+
ensure_ascii=False, indent=2))
|
|
176
|
+
else:
|
|
177
|
+
print(format_report(rep, color=sys.stdout.isatty()))
|
|
178
|
+
return 1 if (args.strict and rep.n_flag) else 0
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
== DENOMINATOR MISMATCH == fixture/dta_bad.md
|
|
2
|
+
mismatches=2
|
|
3
|
+
[MAJOR] DTA_DENOMINATOR_MISMATCH sensitivity denominator 18 (disease-positive) ≠ reference-standard positive count 19 from the characteristics table
|
|
4
|
+
[MAJOR] DTA_DENOMINATOR_MISMATCH specificity denominator 15 (disease-negative) ≠ reference-standard negative count 14 from the characteristics table
|
|
5
|
+
[INFO ] GRAND_TOTAL_AGREES grand totals agree (14+19 = 18+15 = 33), so a totals-only check passes — the split-level mismatch is the defect
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
## Table 1. Reference standard (pathological N stage)
|
|
2
|
+
|
|
3
|
+
| Pathological N stage | Patients |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| pN0 | 14 |
|
|
6
|
+
| pN1 | 13 |
|
|
7
|
+
| pN2 | 6 |
|
|
8
|
+
|
|
9
|
+
## Results
|
|
10
|
+
|
|
11
|
+
Diagnostic performance was assessed in 33 patients. The specificity was 93.3% (14/15)
|
|
12
|
+
and the sensitivity was 72.2% (13/18).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
## Table 1. Reference standard (pathological N stage)
|
|
2
|
+
|
|
3
|
+
| Pathological N stage | Patients |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| pN0 | 14 |
|
|
6
|
+
| pN1 | 13 |
|
|
7
|
+
| pN2 | 6 |
|
|
8
|
+
|
|
9
|
+
## Results
|
|
10
|
+
|
|
11
|
+
Diagnostic performance was assessed in 33 patients. The specificity was 100% (14/14)
|
|
12
|
+
and the sensitivity was 68.4% (13/19).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Challenge card — DTA-denominator gate (self-review)
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
Sensitivity is a/(a+c) over disease-positive patients and specificity d/(b+d) over
|
|
5
|
+
disease-negative patients, so those denominators must equal the reference-standard
|
|
6
|
+
positive / negative counts in the characteristics table. When they disagree while
|
|
7
|
+
the grand total still matches (table 14/19, Results 15/18, both = 33), a totals-only
|
|
8
|
+
check passes and the split-level error — under an Abstract headline — survives
|
|
9
|
+
multiple review rounds. It is present from the first submission, not introduced by a
|
|
10
|
+
revision.
|
|
11
|
+
|
|
12
|
+
## What the gate does
|
|
13
|
+
`check_dta_denominators.py` recovers the reference-standard negative / positive
|
|
14
|
+
counts from the characteristics table (pN0 vs pN1+pN2; node-negative vs -positive;
|
|
15
|
+
absent vs present) and the sensitivity / specificity denominators from the Results
|
|
16
|
+
prose, and asserts each denominator equals its category count — explicitly noting
|
|
17
|
+
when the grand totals agree, because that is what hides it. It also checks a staging
|
|
18
|
+
breakdown sums (`STAGE_ROWSUM`: correct + over + under == n).
|
|
19
|
+
|
|
20
|
+
## Fixture (synthetic only — no real manuscript, no PII)
|
|
21
|
+
- `fixture/dta_bad.md` — table pN0=14 / pN1+pN2=19; Results specificity 14/15 and
|
|
22
|
+
sensitivity 13/18; grand totals both 33.
|
|
23
|
+
- `fixture/dta_ok.md` — Results 14/14 and 13/19 match the table.
|
|
24
|
+
|
|
25
|
+
## Expected
|
|
26
|
+
- `expected/bad.txt` — two `DTA_DENOMINATOR_MISMATCH` + `GRAND_TOTAL_AGREES`; exit 1 under `--strict`.
|
|
27
|
+
- `expected/ok.txt` — `OK`; exit 0.
|
|
28
|
+
|
|
29
|
+
`verify.sh` diffs both outputs and asserts the exit-code contract. Network-free, stdlib-only.
|