medsci-skills 5.19.0 → 5.20.1

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.
Files changed (46) hide show
  1. package/README.md +5 -1
  2. package/metadata/distribution_files.json +189 -14
  3. package/metadata/distribution_manifest.json +1 -1
  4. package/package.json +1 -1
  5. package/skills/orchestrate/SKILL.md +21 -1
  6. package/skills/peer-review/SKILL.md +10 -1
  7. package/skills/peer-review/references/domain-probes/diagnostic_accuracy.md +1 -0
  8. package/skills/peer-review/references/domain-probes/observational_confounding.md +1 -0
  9. package/skills/self-review/references/domain-probes/diagnostic_accuracy.md +1 -0
  10. package/skills/self-review/references/domain-probes/observational_confounding.md +1 -0
  11. package/skills/self-review/scripts/check_dta_denominators.py +182 -0
  12. package/skills/self-review/scripts/check_dta_denominators_challenge/expected/bad.txt +5 -0
  13. package/skills/self-review/scripts/check_dta_denominators_challenge/expected/ok.txt +3 -0
  14. package/skills/self-review/scripts/check_dta_denominators_challenge/fixture/dta_bad.md +12 -0
  15. package/skills/self-review/scripts/check_dta_denominators_challenge/fixture/dta_ok.md +12 -0
  16. package/skills/self-review/scripts/check_dta_denominators_challenge/problem.md +29 -0
  17. package/skills/self-review/scripts/check_dta_denominators_challenge/verify.sh +19 -0
  18. package/skills/self-review/scripts/check_nested_group_comparison.py +154 -0
  19. package/skills/self-review/scripts/check_nested_group_comparison_challenge/expected/clean.txt +3 -0
  20. package/skills/self-review/scripts/check_nested_group_comparison_challenge/expected/nested.txt +3 -0
  21. package/skills/self-review/scripts/check_nested_group_comparison_challenge/fixture/clean.md +11 -0
  22. package/skills/self-review/scripts/check_nested_group_comparison_challenge/fixture/nested.md +6 -0
  23. package/skills/self-review/scripts/check_nested_group_comparison_challenge/problem.md +27 -0
  24. package/skills/self-review/scripts/check_nested_group_comparison_challenge/verify.sh +20 -0
  25. package/skills/self-review/scripts/check_paired_difference_estimator.py +149 -0
  26. package/skills/self-review/scripts/check_paired_difference_estimator_challenge/expected/bad.txt +5 -0
  27. package/skills/self-review/scripts/check_paired_difference_estimator_challenge/expected/ok.txt +3 -0
  28. package/skills/self-review/scripts/check_paired_difference_estimator_challenge/fixture/paired_bad.md +8 -0
  29. package/skills/self-review/scripts/check_paired_difference_estimator_challenge/fixture/paired_ok.md +9 -0
  30. package/skills/self-review/scripts/check_paired_difference_estimator_challenge/problem.md +26 -0
  31. package/skills/self-review/scripts/check_paired_difference_estimator_challenge/verify.sh +20 -0
  32. package/skills/self-review/scripts/check_reported_p_from_counts.py +214 -0
  33. package/skills/self-review/scripts/check_reported_p_from_counts_challenge/expected/bad.txt +3 -0
  34. package/skills/self-review/scripts/check_reported_p_from_counts_challenge/expected/ok.txt +3 -0
  35. package/skills/self-review/scripts/check_reported_p_from_counts_challenge/fixture/p_bad.md +6 -0
  36. package/skills/self-review/scripts/check_reported_p_from_counts_challenge/fixture/p_ok.md +6 -0
  37. package/skills/self-review/scripts/check_reported_p_from_counts_challenge/problem.md +29 -0
  38. package/skills/self-review/scripts/check_reported_p_from_counts_challenge/verify.sh +20 -0
  39. package/skills/self-review/scripts/check_table_percentages.py +211 -0
  40. package/skills/self-review/scripts/check_table_percentages_challenge/expected/bad.txt +6 -0
  41. package/skills/self-review/scripts/check_table_percentages_challenge/expected/ok.txt +3 -0
  42. package/skills/self-review/scripts/check_table_percentages_challenge/fixture/table_bad.md +6 -0
  43. package/skills/self-review/scripts/check_table_percentages_challenge/fixture/table_ok.md +11 -0
  44. package/skills/self-review/scripts/check_table_percentages_challenge/problem.md +34 -0
  45. package/skills/self-review/scripts/check_table_percentages_challenge/verify.sh +40 -0
  46. package/skills/self-review/skill.yml +11 -0
package/README.md CHANGED
@@ -288,6 +288,10 @@ The E2E pipeline (`orchestrate --e2e`) produces everything up to `qc/`. The `sub
288
288
 
289
289
  ## What's New
290
290
 
291
+ **v5.20.1** — audit-driven coherence fixes. A real `/orchestrate --e2e` state-transition bug (the pipeline halted at step 3 because it required a DOCX that is only rendered at step 7), all 55 skills made routable from the single entry point with a CI reachability gate, and a README plugin-count that had drifted from the marketplace SSOT (now gated). No skill/detector change.
292
+
293
+ **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.)
294
+
291
295
  **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
296
 
293
297
  **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.)
@@ -354,7 +358,7 @@ The E2E pipeline (`orchestrate --e2e`) produces everything up to `qc/`. The `sub
354
358
 
355
359
  **v4.1** ships distribution levers and a submission pre-flight gate — analysis-integrity detectors **24 → 25** (still 43 skills):
356
360
 
357
- - **Claude Code plugin marketplace** — `/plugin marketplace add Aperivue/medsci-skills`, then `/plugin` discovery of eight `medsci-*` category plugins generated from the catalog SSOT (`.claude-plugin/marketplace.json`).
361
+ - **Claude Code plugin marketplace** — `/plugin marketplace add Aperivue/medsci-skills`, then `/plugin` discovery of nine `medsci-*` category plugins generated from the catalog SSOT (`.claude-plugin/marketplace.json`).
358
362
  - **MedSci-Audit detector registry** — the deterministic verification layer is now a named, enumerated, citable suite ([`MEDSCI_AUDIT.md`](MEDSCI_AUDIT.md) + generated `metadata/detectors_catalog.json`, six audit families).
359
363
  - **Hero-skill standalone mirrors** — `scripts/sync_hero_skill.py` mirrors a focused skill to its own star-funnel repo; first two live: [`Aperivue/verify-refs`](https://github.com/Aperivue/verify-refs) and [`Aperivue/check-reporting`](https://github.com/Aperivue/check-reporting).
360
364
  - **Placeholder/marker gate** — `check_placeholders.py` flags leftover `[@NEW:]` / `[version]` / `TODO` / template-URL markers before submission (the 25th detector).
@@ -3023,8 +3023,8 @@
3023
3023
  },
3024
3024
  {
3025
3025
  "path": "skills/orchestrate/SKILL.md",
3026
- "size": 35203,
3027
- "sha256": "ef8b919606efbf794d7ebe67b011bd676c129cd1d9d51bac16d85dcaaae5e12e"
3026
+ "size": 40144,
3027
+ "sha256": "6535b55a55791f1339c32d4b8ee8e7ebdaf749a6c3dc1677ef8f3a653ef424ca"
3028
3028
  },
3029
3029
  {
3030
3030
  "path": "skills/orchestrate/references/dialogue_nodes.md",
@@ -3048,8 +3048,8 @@
3048
3048
  },
3049
3049
  {
3050
3050
  "path": "skills/peer-review/SKILL.md",
3051
- "size": 72603,
3052
- "sha256": "92b89c3edc60c1d621738904df2bd4932fcb70babc60e3e306d857c781cfd6e6"
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": 11967,
3077
- "sha256": "8c0a8bd30fe16507e2997ddba1bbcfb4ee7eade7df44f1981e1427cd3a28d233"
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": 34795,
3122
- "sha256": "ff8cb910c7fb83ec08a04ba88866bdf1e0d2f43d972e2861b825595a85b035ee"
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": 11967,
3782
- "sha256": "8c0a8bd30fe16507e2997ddba1bbcfb4ee7eade7df44f1981e1427cd3a28d233"
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": 34795,
3827
- "sha256": "ff8cb910c7fb83ec08a04ba88866bdf1e0d2f43d972e2861b825595a85b035ee"
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": 2381,
4022
- "sha256": "9499a87d985d981eac9418d6cd95ed79c1d9724aba8854b7d6713d89679d9f1d"
4196
+ "size": 3278,
4197
+ "sha256": "84ea9430003ffc8f1b2e009600ac3b4614abb84a697ec1891a39086aa65fe404"
4023
4198
  },
4024
4199
  {
4025
4200
  "path": "skills/setup-medsci/SKILL.md",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "version": "5.19.0",
3
+ "version": "5.20.1",
4
4
  "owned_skills": [
5
5
  "academic-aio",
6
6
  "add-journal",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "medsci-skills",
3
- "version": "5.19.0",
3
+ "version": "5.20.1",
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",
@@ -73,6 +73,26 @@ You do NOT do the work yourself. You classify, plan, and delegate.
73
73
  | **fill-icmje-coi** | Form filling | Batch ICMJE COI Disclosure Form generation per author from a synthetic seed |
74
74
  | **sync-submission** | Submission | SSOT-to-submission drift audit; journal-specific submission manifest creation from canonical manuscript artifacts |
75
75
  | **peer-review** | Review | External manuscript peer review draft generation (journal-specific formatting). Use ONLY for reviewing other authors' work, never for self-review |
76
+ | **review-paper** | Writing | Scaffold/draft a literature review (narrative / scoping PRISMA-ScR / systematic); reuses the self-review narrative-review probes for QC. Distinct from `write-paper` (original research) and `meta-analysis` (pooling) |
77
+ | **polish-language** | Quality | Academic-English consistency lint + non-native clarity polish (abbreviation define-once, US/UK spelling drift, hyphen/en-dash ranges, P/p case, value/unit spacing). Style-only; distinct from `humanize` (AI-tell removal) and `check-reporting` (guideline items) |
78
+ | **author-strategy** | Analysis | PubMed author-profile analysis: study-type classification, trajectory-archetype, publication-strategy report from a name |
79
+ | **batch-cohort** | Analysis | Generate N analysis scripts from one validated methodology template × many exposure/outcome combinations (same method, swap variables) + summary matrix |
80
+ | **replicate-study** | Analysis | Replicate an existing cohort study's methodology on a different database: design extraction, variable-harmonization table, replication-difference report |
81
+ | **cross-national** | Analysis | Cross-national comparison study (KNHANES + NHANES + CHNS or parallel surveys): variable harmonization + parallel weighted analysis |
82
+ | **ma-scout** | Systematic review | Meta-analysis topic discovery + feasibility (professor-first profile→gap, or topic-first question→gap→co-author) before a protocol exists |
83
+ | **find-cohort-gap** | Methodology | Research-gap discovery from a longitudinal cohort DB: profile strengths, match PI expertise, literature-saturation scan, ranked topic proposals |
84
+ | **design-ai-benchmarking** | Methodology | Design/validity review for benchmarking one or more AI systems against a human-expert reference panel (decoupled rubrics, planted calibration probes, reviewer-panel construction, IRR targets, rating-export schema) — before data collection |
85
+ | **architecture-zoo** | Modeling | Choose a medical-imaging model architecture (classification / segmentation / detection / transfer) before scaffolding — maps task + modality + labelled-data scale + imbalance to a paper-grounded shortlist |
86
+ | **preprocess-imaging** | Modeling | Design/audit DICOM/NIfTI intake, resampling, normalisation, and augmentation so the pipeline is leakage-safe before `model-scaffold`; emits a preprocessing manifest + data-stage leakage gate |
87
+ | **model-scaffold** | Modeling | Generate a reproducible runnable PyTorch training repo (patient-level seed-locked split, task model, train/eval scripts, repro record) — the link between choosing an architecture and validating a trained model |
88
+ | **radiomics-ml** | Modeling | Produce/audit a radiomics / tabular-ML study (imaging or clinical features → penalised logistic / SVM / RF / gradient-boosting / MLP → outcome) with a learner-agnostic nested-CV / feature-stability / calibration / external-validation gate (no GPU) |
89
+ | **model-validation** | Validation | Design/audit the clinical-validation study for an engineer-built imaging model (segmentation / classification / detection): patient-level split disjointness, internal-vs-external validation, comparator, metric fit — with a deterministic split-leakage gate |
90
+ | **model-evaluation** | Evaluation | Compute task-correct held-out metrics for a trained imaging model (segmentation Dice + boundary; classification AUROC + AUPRC + Se/Sp with bootstrap CIs; detection FROC/mAP; calibration; subgroup slices) → per-case results table |
91
+ | **mllm-eval** | Evaluation | Design/audit a model-agnostic evaluation harness for an LLM/MLLM clinical task (report generation, VQA, extraction/classification): adjudicated reference, clinical-efficacy metrics beyond BLEU/ROUGE, hallucination, contamination, prompt-sensitivity, reader study |
92
+ | **explainability** | Modeling | Produce/audit a medical-imaging model's interpretability analysis (Grad-CAM / saliency / integrated-gradients) held to the reviewer bar — Adebayo sanity checks, quantitative localisation vs ground truth, cohort-level results, attribution-not-validation framing |
93
+ | **uncertainty-imaging** | Modeling | Design/audit the uncertainty-quantification / OOD-detection / selective-prediction layer of a deployment-framed imaging model (MC-dropout / ensemble / conformal, held-out OOD set, abstention at a pre-specified point) + deployment-claim gate |
94
+ | **model-card** | Documentation | Generate a Model Card + Datasheet + METRIC-informed data-quality pass for an engineer-built imaging model, filled from user-supplied facts, with a completeness gate (never fabricates numbers/provenance/licence) |
95
+ | **setup-medsci** | Setup | Diagnostic checklist for the runtime (Python, R, Node, Claude Code, Git, Zotero, MCP servers) — read-only pass/fail table pointing to the right setup doc for any missing component |
76
96
 
77
97
  ---
78
98
 
@@ -287,7 +307,7 @@ After each skill completes, verify that expected output files exist. If validati
287
307
  |-------|-----------------|------------|
288
308
  | `/analyze-stats` | At least one file in `analysis/tables/*.csv` OR `analysis/_analysis_outputs.md` | Check file existence and non-empty |
289
309
  | `/make-figures` | `analysis/figures/_figure_manifest.md` with at least 1 entry | Parse manifest, verify listed files exist |
290
- | `/write-paper` | `manuscript/manuscript.md` (required), `manuscript/manuscript_final.docx` (required in --e2e) | Check file existence and non-empty |
310
+ | `/write-paper` | `manuscript/manuscript.md` (required) | Check file existence and non-empty. Do NOT require the DOCX here — `manuscript_final.docx` is rendered later by `/manage-refs` (step 7); requiring it at this step would halt an `--e2e` run before the DOCX exists |
291
311
  | `/check-reporting` | `qc/reporting_checklist.md` or inline report | Check file existence |
292
312
  | `/verify-refs` | `qc/reference_audit.json` (sole output) | Parse JSON; halt if `submission_safe == false` (i.e., `FABRICATED` / `MISMATCH` count > 0 OR `duplicate_findings[]` nonempty) |
293
313
  | `/self-review` | Review report with JSON block (when --json) | Check JSON block is parseable. In `--panel` mode each issue may carry an additional optional `consensus` array plus R1/R2/R3 attribution annotations on the `M`/`m` comments — these are additive and backwards-compatible; accept them |
@@ -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())