manuscript-guard 0.2.431__tar.gz

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 (268) hide show
  1. manuscript_guard-0.2.431/.claude-plugin/marketplace.json +23 -0
  2. manuscript_guard-0.2.431/.gitattributes +25 -0
  3. manuscript_guard-0.2.431/.github/ISSUE_TEMPLATE/design-claim-is-untrue.md +12 -0
  4. manuscript_guard-0.2.431/.github/ISSUE_TEMPLATE/gate-got-it-wrong.md +23 -0
  5. manuscript_guard-0.2.431/.github/scripts/check_wheel_files.py +87 -0
  6. manuscript_guard-0.2.431/.github/scripts/release_decision.py +101 -0
  7. manuscript_guard-0.2.431/.github/workflows/ci.yml +204 -0
  8. manuscript_guard-0.2.431/.github/workflows/publish.yml +137 -0
  9. manuscript_guard-0.2.431/.gitignore +50 -0
  10. manuscript_guard-0.2.431/ATTRIBUTION.md +194 -0
  11. manuscript_guard-0.2.431/CITATION.cff +28 -0
  12. manuscript_guard-0.2.431/CLAUDE.md +178 -0
  13. manuscript_guard-0.2.431/CONTRIBUTING.md +84 -0
  14. manuscript_guard-0.2.431/DESIGN.md +7343 -0
  15. manuscript_guard-0.2.431/LICENSE +21 -0
  16. manuscript_guard-0.2.431/PKG-INFO +377 -0
  17. manuscript_guard-0.2.431/README.md +329 -0
  18. manuscript_guard-0.2.431/docs/agent-tools.md +190 -0
  19. manuscript_guard-0.2.431/docs/audit.md +39 -0
  20. manuscript_guard-0.2.431/docs/img/check-dark.svg +13 -0
  21. manuscript_guard-0.2.431/docs/img/check-light.svg +13 -0
  22. manuscript_guard-0.2.431/docs/img/loop-dark.svg +38 -0
  23. manuscript_guard-0.2.431/docs/img/loop-light.svg +38 -0
  24. manuscript_guard-0.2.431/docs/install.md +114 -0
  25. manuscript_guard-0.2.431/docs/principles.md +143 -0
  26. manuscript_guard-0.2.431/docs/review-panel.md +71 -0
  27. manuscript_guard-0.2.431/docs/stages.md +36 -0
  28. manuscript_guard-0.2.431/example/AGENTS.md +29 -0
  29. manuscript_guard-0.2.431/example/analysis/00_simulate.py +58 -0
  30. manuscript_guard-0.2.431/example/analysis/01_disproportionality.py +146 -0
  31. manuscript_guard-0.2.431/example/authors.yaml +37 -0
  32. manuscript_guard-0.2.431/example/data/reports.csv +4001 -0
  33. manuscript_guard-0.2.431/example/design/plan.md +67 -0
  34. manuscript_guard-0.2.431/example/figures/forest.guard.yaml +19 -0
  35. manuscript_guard-0.2.431/example/figures/forest.png +0 -0
  36. manuscript_guard-0.2.431/example/figures/forest.py +102 -0
  37. manuscript_guard-0.2.431/example/figures/forest.render.json +9 -0
  38. manuscript_guard-0.2.431/example/figures/forest.review.yaml +52 -0
  39. manuscript_guard-0.2.431/example/figures/forest.svg +252 -0
  40. manuscript_guard-0.2.431/example/literature/attested.yaml +18 -0
  41. manuscript_guard-0.2.431/example/literature/ledger.yaml +28 -0
  42. manuscript_guard-0.2.431/example/literature/references.bib +24 -0
  43. manuscript_guard-0.2.431/example/literature/sources/fictionalClassSignal2019.txt +13 -0
  44. manuscript_guard-0.2.431/example/literature/sources/fictionalHepaticCohort2021.txt +13 -0
  45. manuscript_guard-0.2.431/example/manuscript/main.md +119 -0
  46. manuscript_guard-0.2.431/example/manuscript/supplementary/S1_code_lists.md +37 -0
  47. manuscript_guard-0.2.431/example/methods.lock +11 -0
  48. manuscript_guard-0.2.431/example/paper.yaml +44 -0
  49. manuscript_guard-0.2.431/example/profiles/journals/demo-journal.yaml +54 -0
  50. manuscript_guard-0.2.431/example/profiles/reporting/DEMO-OBS.yaml +76 -0
  51. manuscript_guard-0.2.431/example/reporting/DEMO-OBS.yaml +39 -0
  52. manuscript_guard-0.2.431/example/results/01_disproportionality.json +430 -0
  53. manuscript_guard-0.2.431/example/results/01_disproportionality.json.sha256 +1 -0
  54. manuscript_guard-0.2.431/example/review/panel-1.yaml +29 -0
  55. manuscript_guard-0.2.431/example/review/panel-2.yaml +27 -0
  56. manuscript_guard-0.2.431/example/review/round-1/biostatistician.yaml +38 -0
  57. manuscript_guard-0.2.431/example/review/round-1/checklist-auditor.yaml +33 -0
  58. manuscript_guard-0.2.431/example/review/round-1/pharmacoepidemiologist.yaml +52 -0
  59. manuscript_guard-0.2.431/example/review/round-2/clinical-reader.yaml +36 -0
  60. manuscript_guard-0.2.431/example/review/round-2/desk-editor.yaml +29 -0
  61. manuscript_guard-0.2.431/plugin/.claude-plugin/plugin.json +18 -0
  62. manuscript_guard-0.2.431/plugin/hooks/hooks.json +53 -0
  63. manuscript_guard-0.2.431/plugin/skills/analysis-plan/SKILL.md +106 -0
  64. manuscript_guard-0.2.431/plugin/skills/figure-review/SKILL.md +102 -0
  65. manuscript_guard-0.2.431/plugin/skills/journal-profile/SKILL.md +141 -0
  66. manuscript_guard-0.2.431/plugin/skills/literature-verify/SKILL.md +121 -0
  67. manuscript_guard-0.2.431/plugin/skills/manuscript-writing/SKILL.md +205 -0
  68. manuscript_guard-0.2.431/plugin/skills/methods-writer/SKILL.md +88 -0
  69. manuscript_guard-0.2.431/plugin/skills/paper-audit/SKILL.md +186 -0
  70. manuscript_guard-0.2.431/plugin/skills/project-setup/SKILL.md +196 -0
  71. manuscript_guard-0.2.431/plugin/skills/reporting-checklist/SKILL.md +102 -0
  72. manuscript_guard-0.2.431/plugin/skills/results-binding/SKILL.md +203 -0
  73. manuscript_guard-0.2.431/plugin/skills/review-panel/SKILL.md +202 -0
  74. manuscript_guard-0.2.431/plugin/skills/reviewer-response/SKILL.md +189 -0
  75. manuscript_guard-0.2.431/plugin/skills/submission-pack/SKILL.md +77 -0
  76. manuscript_guard-0.2.431/plugin/skills/word-roundtrip/SKILL.md +342 -0
  77. manuscript_guard-0.2.431/pyproject.toml +119 -0
  78. manuscript_guard-0.2.431/r/manuscriptguard/DESCRIPTION +16 -0
  79. manuscript_guard-0.2.431/r/manuscriptguard/LICENSE +2 -0
  80. manuscript_guard-0.2.431/r/manuscriptguard/NAMESPACE +4 -0
  81. manuscript_guard-0.2.431/r/manuscriptguard/R/emit.R +735 -0
  82. manuscript_guard-0.2.431/src/manuscript_guard/__init__.py +13 -0
  83. manuscript_guard-0.2.431/src/manuscript_guard/annotate.py +744 -0
  84. manuscript_guard-0.2.431/src/manuscript_guard/audit.py +912 -0
  85. manuscript_guard-0.2.431/src/manuscript_guard/binding.py +196 -0
  86. manuscript_guard-0.2.431/src/manuscript_guard/build/__init__.py +48 -0
  87. manuscript_guard-0.2.431/src/manuscript_guard/build/assemble.py +359 -0
  88. manuscript_guard-0.2.431/src/manuscript_guard/build/document.py +783 -0
  89. manuscript_guard-0.2.431/src/manuscript_guard/build/figure_caption.lua +81 -0
  90. manuscript_guard-0.2.431/src/manuscript_guard/build/reading.py +755 -0
  91. manuscript_guard-0.2.431/src/manuscript_guard/build/styles.py +129 -0
  92. manuscript_guard-0.2.431/src/manuscript_guard/build/submission.py +481 -0
  93. manuscript_guard-0.2.431/src/manuscript_guard/build/tex_check.py +205 -0
  94. manuscript_guard-0.2.431/src/manuscript_guard/build/zotero_word.lua +100 -0
  95. manuscript_guard-0.2.431/src/manuscript_guard/classify.py +557 -0
  96. manuscript_guard-0.2.431/src/manuscript_guard/cli.py +2481 -0
  97. manuscript_guard-0.2.431/src/manuscript_guard/contracts/__init__.py +62 -0
  98. manuscript_guard-0.2.431/src/manuscript_guard/contracts/_schema.py +166 -0
  99. manuscript_guard-0.2.431/src/manuscript_guard/contracts/literature.py +122 -0
  100. manuscript_guard-0.2.431/src/manuscript_guard/contracts/project.py +701 -0
  101. manuscript_guard-0.2.431/src/manuscript_guard/contracts/results.py +215 -0
  102. manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/attested.schema.json +52 -0
  103. manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/authors.schema.json +95 -0
  104. manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/figure_review.schema.json +65 -0
  105. manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/journal.schema.json +95 -0
  106. manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/ledger.schema.json +57 -0
  107. manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/panel.schema.json +61 -0
  108. manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/paper.schema.json +163 -0
  109. manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/reporting.schema.json +51 -0
  110. manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/reporting_completion.schema.json +33 -0
  111. manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/results.schema.json +207 -0
  112. manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/review.schema.json +129 -0
  113. manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/review_reply.schema.json +59 -0
  114. manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/revision.schema.json +93 -0
  115. manuscript_guard-0.2.431/src/manuscript_guard/contracts/values.py +263 -0
  116. manuscript_guard-0.2.431/src/manuscript_guard/data/abbreviations.yaml +56 -0
  117. manuscript_guard-0.2.431/src/manuscript_guard/data/ai_writing.yaml +190 -0
  118. manuscript_guard-0.2.431/src/manuscript_guard/data/conventions.yaml +133 -0
  119. manuscript_guard-0.2.431/src/manuscript_guard/data/plot_params.yaml +179 -0
  120. manuscript_guard-0.2.431/src/manuscript_guard/data/spelling_variants.tsv +8436 -0
  121. manuscript_guard-0.2.431/src/manuscript_guard/data/structural.yaml +262 -0
  122. manuscript_guard-0.2.431/src/manuscript_guard/data/terms.yaml +203 -0
  123. manuscript_guard-0.2.431/src/manuscript_guard/docxtext.py +909 -0
  124. manuscript_guard-0.2.431/src/manuscript_guard/emit.py +827 -0
  125. manuscript_guard-0.2.431/src/manuscript_guard/findings.py +143 -0
  126. manuscript_guard-0.2.431/src/manuscript_guard/gates/__init__.py +81 -0
  127. manuscript_guard-0.2.431/src/manuscript_guard/gates/citations.py +215 -0
  128. manuscript_guard-0.2.431/src/manuscript_guard/gates/consistency.py +135 -0
  129. manuscript_guard-0.2.431/src/manuscript_guard/gates/design.py +185 -0
  130. manuscript_guard-0.2.431/src/manuscript_guard/gates/figure_review.py +171 -0
  131. manuscript_guard-0.2.431/src/manuscript_guard/gates/figure_source.py +310 -0
  132. manuscript_guard-0.2.431/src/manuscript_guard/gates/figures.py +408 -0
  133. manuscript_guard-0.2.431/src/manuscript_guard/gates/freshness.py +253 -0
  134. manuscript_guard-0.2.431/src/manuscript_guard/gates/journal.py +277 -0
  135. manuscript_guard-0.2.431/src/manuscript_guard/gates/language.py +848 -0
  136. manuscript_guard-0.2.431/src/manuscript_guard/gates/literature.py +176 -0
  137. manuscript_guard-0.2.431/src/manuscript_guard/gates/methods.py +202 -0
  138. manuscript_guard-0.2.431/src/manuscript_guard/gates/numbers.py +847 -0
  139. manuscript_guard-0.2.431/src/manuscript_guard/gates/reporting.py +344 -0
  140. manuscript_guard-0.2.431/src/manuscript_guard/gates/review.py +821 -0
  141. manuscript_guard-0.2.431/src/manuscript_guard/gates/revision.py +334 -0
  142. manuscript_guard-0.2.431/src/manuscript_guard/gates/spelling.py +279 -0
  143. manuscript_guard-0.2.431/src/manuscript_guard/gates/vocabulary.py +334 -0
  144. manuscript_guard-0.2.431/src/manuscript_guard/gates/writing.py +265 -0
  145. manuscript_guard-0.2.431/src/manuscript_guard/hooks.py +778 -0
  146. manuscript_guard-0.2.431/src/manuscript_guard/literature/__init__.py +19 -0
  147. manuscript_guard-0.2.431/src/manuscript_guard/literature/sources.py +181 -0
  148. manuscript_guard-0.2.431/src/manuscript_guard/merge.py +2433 -0
  149. manuscript_guard-0.2.431/src/manuscript_guard/panel/__init__.py +17 -0
  150. manuscript_guard-0.2.431/src/manuscript_guard/panel/client.py +513 -0
  151. manuscript_guard-0.2.431/src/manuscript_guard/panel/commands.py +331 -0
  152. manuscript_guard-0.2.431/src/manuscript_guard/panel/plan.py +535 -0
  153. manuscript_guard-0.2.431/src/manuscript_guard/panel/prompt.py +368 -0
  154. manuscript_guard-0.2.431/src/manuscript_guard/panel/providers.py +350 -0
  155. manuscript_guard-0.2.431/src/manuscript_guard/panel/reply.py +119 -0
  156. manuscript_guard-0.2.431/src/manuscript_guard/panel/run.py +578 -0
  157. manuscript_guard-0.2.431/src/manuscript_guard/paths.py +88 -0
  158. manuscript_guard-0.2.431/src/manuscript_guard/policy.py +219 -0
  159. manuscript_guard-0.2.431/src/manuscript_guard/profiles/journals/TEMPLATE.yaml +210 -0
  160. manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/ARRIVE-2.0.recipe.yaml +33 -0
  161. manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/CONSORT.recipe.yaml +25 -0
  162. manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/PRISMA-2020-abstracts.recipe.yaml +21 -0
  163. manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/PRISMA-2020.recipe.yaml +20 -0
  164. manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/READUS-PV-abstracts.recipe.yaml +19 -0
  165. manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/READUS-PV.recipe.yaml +24 -0
  166. manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/RECORD-PE.recipe.yaml +26 -0
  167. manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/RECORD.recipe.yaml +26 -0
  168. manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/SPIRIT-2025.recipe.yaml +20 -0
  169. manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/STROBE.recipe.yaml +29 -0
  170. manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/TRIPOD-development-validation.recipe.yaml +23 -0
  171. manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/TRIPOD-development.recipe.yaml +24 -0
  172. manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/TRIPOD-validation.recipe.yaml +20 -0
  173. manuscript_guard-0.2.431/src/manuscript_guard/record.py +402 -0
  174. manuscript_guard-0.2.431/src/manuscript_guard/render.py +125 -0
  175. manuscript_guard-0.2.431/src/manuscript_guard/reporting/__init__.py +24 -0
  176. manuscript_guard-0.2.431/src/manuscript_guard/reporting/build.py +138 -0
  177. manuscript_guard-0.2.431/src/manuscript_guard/reporting/columns.py +174 -0
  178. manuscript_guard-0.2.431/src/manuscript_guard/reporting/fetch.py +131 -0
  179. manuscript_guard-0.2.431/src/manuscript_guard/reporting/transcribe.py +288 -0
  180. manuscript_guard-0.2.431/src/manuscript_guard/roundtrip.py +3277 -0
  181. manuscript_guard-0.2.431/src/manuscript_guard/safexml.py +106 -0
  182. manuscript_guard-0.2.431/src/manuscript_guard/scaffold.py +370 -0
  183. manuscript_guard-0.2.431/src/manuscript_guard/skillcopy.py +338 -0
  184. manuscript_guard-0.2.431/src/manuscript_guard/tables.py +255 -0
  185. manuscript_guard-0.2.431/src/manuscript_guard/text/__init__.py +17 -0
  186. manuscript_guard-0.2.431/src/manuscript_guard/text/attributes.py +105 -0
  187. manuscript_guard-0.2.431/src/manuscript_guard/text/blocks.py +1075 -0
  188. manuscript_guard-0.2.431/src/manuscript_guard/text/code.py +245 -0
  189. manuscript_guard-0.2.431/src/manuscript_guard/text/comments.py +368 -0
  190. manuscript_guard-0.2.431/src/manuscript_guard/text/docx.py +371 -0
  191. manuscript_guard-0.2.431/src/manuscript_guard/text/fences.py +540 -0
  192. manuscript_guard-0.2.431/src/manuscript_guard/text/inline.py +318 -0
  193. manuscript_guard-0.2.431/src/manuscript_guard/text/masking.py +775 -0
  194. manuscript_guard-0.2.431/src/manuscript_guard/text/placeholders.py +123 -0
  195. manuscript_guard-0.2.431/src/manuscript_guard/text/sections.py +629 -0
  196. manuscript_guard-0.2.431/src/manuscript_guard/text/tex.py +478 -0
  197. manuscript_guard-0.2.431/src/manuscript_guard/text/tokens.py +197 -0
  198. manuscript_guard-0.2.431/src/manuscript_guard/verify.py +464 -0
  199. manuscript_guard-0.2.431/src/manuscript_guard/wordfonts.py +365 -0
  200. manuscript_guard-0.2.431/src/manuscript_guard/zotero/__init__.py +30 -0
  201. manuscript_guard-0.2.431/src/manuscript_guard/zotero/citations.py +129 -0
  202. manuscript_guard-0.2.431/src/manuscript_guard/zotero/client.py +270 -0
  203. manuscript_guard-0.2.431/tests/conftest.py +242 -0
  204. manuscript_guard-0.2.431/tests/data/exemptions.yaml +134 -0
  205. manuscript_guard-0.2.431/tests/data/rule_cases.yaml +423 -0
  206. manuscript_guard-0.2.431/tests/data/timing_budgets.yaml +206 -0
  207. manuscript_guard-0.2.431/tests/test_annotate.py +292 -0
  208. manuscript_guard-0.2.431/tests/test_artifact.py +290 -0
  209. manuscript_guard-0.2.431/tests/test_audit.py +1344 -0
  210. manuscript_guard-0.2.431/tests/test_binding.py +246 -0
  211. manuscript_guard-0.2.431/tests/test_build.py +665 -0
  212. manuscript_guard-0.2.431/tests/test_ci_requirements.py +138 -0
  213. manuscript_guard-0.2.431/tests/test_cli.py +1268 -0
  214. manuscript_guard-0.2.431/tests/test_comments.py +228 -0
  215. manuscript_guard-0.2.431/tests/test_compliance.py +524 -0
  216. manuscript_guard-0.2.431/tests/test_contracts.py +1149 -0
  217. manuscript_guard-0.2.431/tests/test_corruption.py +9443 -0
  218. manuscript_guard-0.2.431/tests/test_docs.py +261 -0
  219. manuscript_guard-0.2.431/tests/test_emit.py +739 -0
  220. manuscript_guard-0.2.431/tests/test_exemptions.py +156 -0
  221. manuscript_guard-0.2.431/tests/test_figures.py +409 -0
  222. manuscript_guard-0.2.431/tests/test_hooks.py +2061 -0
  223. manuscript_guard-0.2.431/tests/test_input_digests.py +279 -0
  224. manuscript_guard-0.2.431/tests/test_interaction.py +138 -0
  225. manuscript_guard-0.2.431/tests/test_language.py +926 -0
  226. manuscript_guard-0.2.431/tests/test_lifecycle.py +251 -0
  227. manuscript_guard-0.2.431/tests/test_literature.py +476 -0
  228. manuscript_guard-0.2.431/tests/test_methods.py +172 -0
  229. manuscript_guard-0.2.431/tests/test_no_duplicate_names.py +234 -0
  230. manuscript_guard-0.2.431/tests/test_ordinary_sessions.py +890 -0
  231. manuscript_guard-0.2.431/tests/test_packaging.py +278 -0
  232. manuscript_guard-0.2.431/tests/test_pandoc_agreement.py +1949 -0
  233. manuscript_guard-0.2.431/tests/test_parity.py +138 -0
  234. manuscript_guard-0.2.431/tests/test_plugin.py +614 -0
  235. manuscript_guard-0.2.431/tests/test_policy.py +471 -0
  236. manuscript_guard-0.2.431/tests/test_printed_record.py +349 -0
  237. manuscript_guard-0.2.431/tests/test_r_emitter.py +461 -0
  238. manuscript_guard-0.2.431/tests/test_raw_tex.py +493 -0
  239. manuscript_guard-0.2.431/tests/test_record.py +790 -0
  240. manuscript_guard-0.2.431/tests/test_review.py +1225 -0
  241. manuscript_guard-0.2.431/tests/test_review_plan.py +934 -0
  242. manuscript_guard-0.2.431/tests/test_review_providers.py +1023 -0
  243. manuscript_guard-0.2.431/tests/test_review_run.py +1316 -0
  244. manuscript_guard-0.2.431/tests/test_review_transport.py +245 -0
  245. manuscript_guard-0.2.431/tests/test_revision.py +380 -0
  246. manuscript_guard-0.2.431/tests/test_robustness.py +1057 -0
  247. manuscript_guard-0.2.431/tests/test_round_five.py +425 -0
  248. manuscript_guard-0.2.431/tests/test_roundtrip.py +12915 -0
  249. manuscript_guard-0.2.431/tests/test_rules.py +179 -0
  250. manuscript_guard-0.2.431/tests/test_scaffold.py +452 -0
  251. manuscript_guard-0.2.431/tests/test_script_signs.py +564 -0
  252. manuscript_guard-0.2.431/tests/test_seed_revision.py +155 -0
  253. manuscript_guard-0.2.431/tests/test_skillcopy.py +751 -0
  254. manuscript_guard-0.2.431/tests/test_spelling.py +1064 -0
  255. manuscript_guard-0.2.431/tests/test_submission.py +426 -0
  256. manuscript_guard-0.2.431/tests/test_tex.py +723 -0
  257. manuscript_guard-0.2.431/tests/test_tex_in_check.py +763 -0
  258. manuscript_guard-0.2.431/tests/test_text.py +710 -0
  259. manuscript_guard-0.2.431/tests/test_timing_budgets.py +739 -0
  260. manuscript_guard-0.2.431/tests/test_transcribe.py +753 -0
  261. manuscript_guard-0.2.431/tests/test_verify.py +264 -0
  262. manuscript_guard-0.2.431/tests/test_version.py +64 -0
  263. manuscript_guard-0.2.431/tests/test_vocabulary.py +772 -0
  264. manuscript_guard-0.2.431/tests/test_writing.py +259 -0
  265. manuscript_guard-0.2.431/tests/test_zotero.py +331 -0
  266. manuscript_guard-0.2.431/tests/test_zotero_word.py +184 -0
  267. manuscript_guard-0.2.431/tools/derive_spelling_variants.py +393 -0
  268. manuscript_guard-0.2.431/tools/readme_figures.py +184 -0
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "manuscript-guard",
3
+ "owner": {
4
+ "name": "Basile Chrétien"
5
+ },
6
+ "metadata": {
7
+ "description": "Toolkit for making every number in a scientific manuscript traceable to its source."
8
+ },
9
+ "plugins": [
10
+ {
11
+ "name": "manuscript-guard",
12
+ "source": "./plugin",
13
+ "description": "Make every number in a scientific manuscript traceable to its source. Skills for setting up a paper project, drafting, literature verification, figure review, journal compliance, review panels, the Word round trip and the response to reviewers; hooks that refuse edits to machine-written files and hold a submission to submission standards.",
14
+ "version": "0.2.431",
15
+ "author": {
16
+ "name": "Basile Chrétien"
17
+ },
18
+ "category": "productivity",
19
+ "homepage": "https://github.com/BasileChretien/manuscript-guard",
20
+ "license": "MIT"
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,25 @@
1
+ # Normalise line endings, and check them out as LF everywhere.
2
+ #
3
+ # This is not cosmetic here. Several of the toolkit's guarantees are byte-level: the
4
+ # .sha256 sidecar beside each results fragment, the analysis digests in methods.lock, the
5
+ # manuscript digest a review record is tied to. With the default behaviour Git stores LF and
6
+ # hands Windows CRLF, so the same commit hashes differently on different machines and every
7
+ # one of those checks reports a change nobody made. CI failed exactly that way on Ubuntu and
8
+ # macOS while passing on the machine the digests were computed on.
9
+ #
10
+ # eol=lf makes the working copy match the repository on every platform, so a digest computed
11
+ # anywhere is valid everywhere.
12
+ * text=auto eol=lf
13
+
14
+ # Binary: never touch these.
15
+ *.docx binary
16
+ *.pdf binary
17
+ *.png binary
18
+ *.jpg binary
19
+ *.jpeg binary
20
+ *.tif binary
21
+ *.tiff binary
22
+ *.eps binary
23
+ *.xlsx binary
24
+ *.zip binary
25
+ *.RData binary
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: DESIGN.md says something the code does not do
3
+ about: A claim in the design document that is not true of the code
4
+ labels: documentation
5
+ ---
6
+
7
+ **The claim** — quote it, with the section heading.
8
+
9
+ **What the code actually does** — a file and line, or a short reproduction.
10
+
11
+ This is the most valuable kind of report here: several real defects have been found hiding
12
+ behind prose that described an intention rather than the behaviour.
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: A gate got it wrong
3
+ about: A number it flagged that is fine, or a defect it let through
4
+ labels: gate
5
+ ---
6
+
7
+ **Which direction**
8
+ - [ ] It flagged something that is not a defect
9
+ - [ ] It let a defect through ← more useful
10
+
11
+ **The smallest text that shows it**
12
+
13
+ ```markdown
14
+ paste the manuscript source, results fragment, or table cell here
15
+ ```
16
+
17
+ **What manuscript-guard said** — the output of `manuscript-guard check`, or
18
+ `manuscript-guard explain <file>` if it is about how a number was classified.
19
+
20
+ **What it should have said, and why.** If this is a convention of your field rather than a
21
+ bug, say which field and where the convention is written down.
22
+
23
+ **Version** — `manuscript-guard --version`, and your OS and Python version.
@@ -0,0 +1,87 @@
1
+ """Compare a built wheel's manuscript_guard files with the ones git tracks in the package.
2
+
3
+ Run by CI's wheel job: `python .github/scripts/check_wheel_files.py <wheel> [<checkout>]`.
4
+
5
+ Using the wheel notices some missing files and not others, because a fresh project reads only
6
+ part of the package's data. Ten of the 32 data files could be dropped from a wheel and the
7
+ job's `init`, `check` and `build --offline` still passed. The two file lists have to be equal,
8
+ so the check names every file that is tracked and not shipped, and every file that is shipped
9
+ and not tracked (a generated checklist profile is one, and carries text that may not be
10
+ redistributed).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import subprocess
16
+ import sys
17
+ import zipfile
18
+ from pathlib import Path
19
+
20
+ IMPORT_NAME = "manuscript_guard/"
21
+ PACKAGE_DIR = "src/manuscript_guard"
22
+
23
+ # Tracked outside the package and taken into it when the wheel is built (pyproject.toml,
24
+ # force-include): where git has them, and where the wheel does.
25
+ FORCED = {"plugin/skills/": "manuscript_guard/skills/"}
26
+
27
+
28
+ def wheel_files(wheel: Path) -> set[str]:
29
+ """The package's files in the wheel, as `manuscript_guard/...` (not the dist-info)."""
30
+ with zipfile.ZipFile(wheel) as archive:
31
+ return {
32
+ name
33
+ for name in archive.namelist()
34
+ if name.startswith(IMPORT_NAME) and not name.endswith("/")
35
+ }
36
+
37
+
38
+ def tracked_files(root: Path) -> set[str]:
39
+ """What git tracks under the package, as `manuscript_guard/...` to match the wheel."""
40
+ listed = subprocess.run(
41
+ ["git", "ls-files", "-z", "--", PACKAGE_DIR, *(place.rstrip("/") for place in FORCED)],
42
+ cwd=root,
43
+ capture_output=True,
44
+ check=True,
45
+ ).stdout.decode("utf-8")
46
+ return {in_the_wheel(name) for name in listed.split("\0") if name}
47
+
48
+
49
+ def in_the_wheel(name: str) -> str:
50
+ """Where a tracked file is expected in the wheel."""
51
+ for tracked, shipped in FORCED.items():
52
+ if name.startswith(tracked):
53
+ return shipped + name[len(tracked) :]
54
+ return name[len("src/") :]
55
+
56
+
57
+ def compare(shipped: set[str], tracked: set[str]) -> tuple[list[str], list[str]]:
58
+ """(tracked but not shipped, shipped but not tracked), each sorted."""
59
+ return sorted(tracked - shipped), sorted(shipped - tracked)
60
+
61
+
62
+ def report(missing: list[str], unexpected: list[str]) -> int:
63
+ for name in missing:
64
+ print(f"missing from the wheel: {name}")
65
+ for name in unexpected:
66
+ print(f"in the wheel, not tracked by git: {name}")
67
+ return 1 if missing or unexpected else 0
68
+
69
+
70
+ def main(argv: list[str]) -> int:
71
+ if len(argv) not in (2, 3):
72
+ print(__doc__, file=sys.stderr)
73
+ return 2
74
+ wheel = Path(argv[1])
75
+ root = Path(argv[2]) if len(argv) == 3 else Path.cwd()
76
+ shipped, tracked = wheel_files(wheel), tracked_files(root)
77
+ if not tracked:
78
+ print(f"git tracks nothing under {PACKAGE_DIR} in {root}", file=sys.stderr)
79
+ return 2
80
+ status = report(*compare(shipped, tracked))
81
+ if status == 0:
82
+ print(f"the wheel holds all {len(tracked)} files git tracks in the package, and no others")
83
+ return status
84
+
85
+
86
+ if __name__ == "__main__":
87
+ sys.exit(main(sys.argv))
@@ -0,0 +1,101 @@
1
+ """Decide whether a push to main carries a version that has to go to PyPI.
2
+
3
+ Run by publish.yml: `python .github/scripts/release_decision.py --before <sha>`. It prints
4
+ two lines for the workflow to read, `publish=true` or `publish=false` and `version=<number>`,
5
+ and one sentence on stderr saying why.
6
+
7
+ Every version on main is released, and a release cannot be taken back: PyPI keeps a version
8
+ for good and refuses the same number twice. So a version is published exactly once, when a
9
+ push changes the number in pyproject.toml to one PyPI does not have. A push that leaves the
10
+ number alone releases nothing, however much else it changes: the code between a merge and
11
+ the bump that follows it carries the old number, and is not that release. Where the answer
12
+ cannot be had, because PyPI did not reply, nothing is published and the run fails.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import re
19
+ import subprocess
20
+ import sys
21
+ import urllib.error
22
+ import urllib.request
23
+ from collections.abc import Callable
24
+ from pathlib import Path
25
+
26
+ PROJECT = "manuscript-guard"
27
+ #: The line of pyproject.toml that gives the version. Read as text, not as TOML, so that the
28
+ #: script runs on every Python the package supports; tests/test_version.py holds the line.
29
+ #: Digits and dots only: the number goes into a tag's name and a line of the workflow's
30
+ #: output, and a release whose number is anything else is not one this project makes.
31
+ VERSION_LINE = re.compile(r'^version\s*=\s*"(\d+(?:\.\d+)+)"\s*$', re.MULTILINE)
32
+ #: What git gives as the commit before the first push of a branch: no commit at all.
33
+ NO_COMMIT = re.compile(r"^0*$")
34
+
35
+
36
+ def version_in(pyproject: str) -> str:
37
+ found = VERSION_LINE.findall(pyproject)
38
+ if len(found) != 1:
39
+ raise SystemExit(f"pyproject.toml gives {len(found)} version lines; one is expected")
40
+ return found[0]
41
+
42
+
43
+ def version_at(commit: str, checkout: Path) -> str | None:
44
+ """The version pyproject.toml gave at a commit, or None where there is none to read."""
45
+ if NO_COMMIT.match(commit):
46
+ return None
47
+ shown = subprocess.run(
48
+ ["git", "show", f"{commit}:pyproject.toml"],
49
+ cwd=checkout,
50
+ capture_output=True,
51
+ text=True,
52
+ encoding="utf-8",
53
+ )
54
+ return version_in(shown.stdout) if shown.returncode == 0 else None
55
+
56
+
57
+ def status_of(url: str) -> int:
58
+ try:
59
+ with urllib.request.urlopen(url, timeout=30) as reply: # noqa: S310 - a fixed https URL
60
+ return reply.status
61
+ except urllib.error.HTTPError as refused:
62
+ return refused.code
63
+
64
+
65
+ def on_pypi(version: str, status: Callable[[str], int] = status_of) -> bool:
66
+ """Whether PyPI already has this version. Anything but a plain yes or no is an error."""
67
+ code = status(f"https://pypi.org/pypi/{PROJECT}/{version}/json")
68
+ if code == 200:
69
+ return True
70
+ if code == 404:
71
+ return False
72
+ raise SystemExit(f"PyPI answered {code} for {PROJECT} {version}: nothing is published")
73
+
74
+
75
+ def decide(now: str, before: str | None, published: Callable[[str], bool]) -> tuple[bool, str]:
76
+ """Whether to publish `now`, and why. `before` is the version of the commit this push
77
+ replaced, or None where there is none to compare with (a run started by hand)."""
78
+ if before == now:
79
+ return False, f"this push left the version at {now}: nothing to release"
80
+ if published(now):
81
+ return False, f"{now} is already on PyPI: nothing to release"
82
+ moved = f"the version went from {before} to {now}" if before else f"the version is {now}"
83
+ return True, f"{moved}, which PyPI does not have: releasing it"
84
+
85
+
86
+ def main(argv: list[str]) -> int:
87
+ parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
88
+ parser.add_argument("--before", default="", help="the commit this push replaced")
89
+ parser.add_argument("--checkout", type=Path, default=Path.cwd())
90
+ args = parser.parse_args(argv[1:])
91
+
92
+ now = version_in((args.checkout / "pyproject.toml").read_text(encoding="utf-8"))
93
+ publish, why = decide(now, version_at(args.before, args.checkout), on_pypi)
94
+ print(why, file=sys.stderr)
95
+ print(f"publish={'true' if publish else 'false'}")
96
+ print(f"version={now}")
97
+ return 0
98
+
99
+
100
+ if __name__ == "__main__":
101
+ sys.exit(main(sys.argv))
@@ -0,0 +1,204 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ env:
9
+ # The build, the import round trip and the checks on what pandoc prints all need pandoc,
10
+ # and each skips without it: before this was set, no test job had pandoc, and none failed
11
+ # for want of it. Every job that runs pytest installs this version, and with the variable
12
+ # set, tests/conftest.py refuses to start unless it is the pandoc on PATH. Set here rather
13
+ # than per step, so a pytest step added later cannot forget it. Pinned, because those
14
+ # tests assert on pandoc's output and a release may print it differently: this is the
15
+ # version they were written against.
16
+ MANUSCRIPT_GUARD_REQUIRE_PANDOC: "3.9.0.2"
17
+
18
+ jobs:
19
+ test:
20
+ # The gates must hold on the platforms other scientists actually use, and only Windows
21
+ # is available to the author, so the other two are covered here rather than assumed.
22
+ strategy:
23
+ fail-fast: false
24
+ matrix:
25
+ os: [ubuntu-latest, windows-latest, macos-latest]
26
+ python: ["3.10", "3.13"]
27
+ runs-on: ${{ matrix.os }}
28
+ # A suite that stalls would hold its runner for GitHub's own six hours. The slowest
29
+ # healthy job, on Windows, takes about 47 minutes, so twice that is a stall.
30
+ timeout-minutes: 90
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+ - uses: actions/setup-python@v5
34
+ with:
35
+ python-version: ${{ matrix.python }}
36
+ - uses: r-lib/actions/setup-pandoc@v2
37
+ with:
38
+ pandoc-version: ${{ env.MANUSCRIPT_GUARD_REQUIRE_PANDOC }}
39
+ - run: pandoc --version
40
+ - run: python -m pip install --upgrade pip
41
+ - run: pip install -e ".[dev]"
42
+ - run: ruff check src tests
43
+ # -rs names each test that still skips, and why.
44
+ - run: pytest -q -rs
45
+
46
+ plugin-manifests:
47
+ # Validation reads JSON only: no model and no credentials are involved. Two versions,
48
+ # because they disagree: 2.1.119 refuses root keys ($schema, description) that the
49
+ # latest release accepts, so a manifest checked only against the latest can still fail
50
+ # for anyone who has not updated. 2.1.119 is the oldest version the plugin was verified
51
+ # with, and has no --strict; on the latest, --strict turns unknown keys into failures.
52
+ strategy:
53
+ fail-fast: false
54
+ matrix:
55
+ claude: ["2.1.119", "latest"]
56
+ runs-on: ubuntu-latest
57
+ steps:
58
+ - uses: actions/checkout@v4
59
+ - uses: actions/setup-node@v4
60
+ with:
61
+ node-version: "22"
62
+ - run: npm install -g @anthropic-ai/claude-code@${{ matrix.claude }}
63
+ - run: claude plugin validate ${{ matrix.claude == 'latest' && '--strict' || '' }} .
64
+ - run: claude plugin validate ${{ matrix.claude == 'latest' && '--strict' || '' }} plugin
65
+
66
+ codex-plugin:
67
+ # Codex installs the plugin from the manifests Claude Code reads, and the README tells a
68
+ # Codex user to do exactly that. `codex plugin` writes only under CODEX_HOME: no model and
69
+ # no login are involved. Two versions, as for Claude Code: the release this was first
70
+ # run against here, and the latest, so that a change in how Codex reads a marketplace
71
+ # fails here before a user meets it. With the variable set, a missing Codex fails the
72
+ # run where it would otherwise skip the test.
73
+ strategy:
74
+ fail-fast: false
75
+ matrix:
76
+ codex: ["0.160.0", "latest"]
77
+ runs-on: ubuntu-latest
78
+ env:
79
+ MANUSCRIPT_GUARD_REQUIRE_CODEX: "1"
80
+ steps:
81
+ - uses: actions/checkout@v4
82
+ - uses: actions/setup-node@v4
83
+ with:
84
+ node-version: "22"
85
+ - run: npm install -g @openai/codex@${{ matrix.codex }}
86
+ - run: codex --version
87
+ - uses: actions/setup-python@v5
88
+ with:
89
+ python-version: "3.12"
90
+ # These tests call no pandoc, but every pytest run here is held to the same requirement.
91
+ - uses: r-lib/actions/setup-pandoc@v2
92
+ with:
93
+ pandoc-version: ${{ env.MANUSCRIPT_GUARD_REQUIRE_PANDOC }}
94
+ - run: pip install -e ".[dev]"
95
+ - run: pytest -q -rs tests/test_plugin.py -k "codex or hooks_file"
96
+
97
+ r-interop:
98
+ # The results contract is only a contract if a second language can honour it.
99
+ runs-on: ubuntu-latest
100
+ steps:
101
+ - uses: actions/checkout@v4
102
+ - uses: actions/setup-python@v5
103
+ with:
104
+ python-version: "3.12"
105
+ - uses: r-lib/actions/setup-r@v2
106
+ - uses: r-lib/actions/setup-r-dependencies@v2
107
+ with:
108
+ packages: jsonlite, digest
109
+ # test_r_emitter.py calls no pandoc, but every pytest run here runs under the same
110
+ # requirement, and setup-r-dependencies leaves apt's older pandoc on PATH, which the
111
+ # requirement refuses. One rule for every job, rather than an exception to remember.
112
+ - uses: r-lib/actions/setup-pandoc@v2
113
+ with:
114
+ pandoc-version: ${{ env.MANUSCRIPT_GUARD_REQUIRE_PANDOC }}
115
+ - run: pip install -e ".[dev]"
116
+ - run: pytest -q tests/test_r_emitter.py
117
+
118
+ wheel:
119
+ # Every other job installs editable, which reads src/ where it stands. A data file, a schema
120
+ # or a recipe that git ignores or the build leaves out is present there and missing from
121
+ # `pip install git+https://...`, which is what a user runs. So build the wheel, install it
122
+ # into a fresh venv, run from a directory that is not the checkout, and use the new project
123
+ # the way the README does.
124
+ strategy:
125
+ fail-fast: false
126
+ matrix:
127
+ os: [ubuntu-latest, windows-latest, macos-latest]
128
+ runs-on: ${{ matrix.os }}
129
+ steps:
130
+ - uses: actions/checkout@v4
131
+ - uses: actions/setup-python@v5
132
+ with:
133
+ python-version: "3.12"
134
+ - uses: r-lib/actions/setup-pandoc@v2
135
+ with:
136
+ pandoc-version: ${{ env.MANUSCRIPT_GUARD_REQUIRE_PANDOC }}
137
+ - run: pandoc --version
138
+ - name: Build the wheel
139
+ shell: bash
140
+ run: |
141
+ python -m pip install --upgrade pip
142
+ pip wheel . --no-deps --wheel-dir "$RUNNER_TEMP/wheelhouse"
143
+ - name: The wheel holds every file git tracks in the package, and no others
144
+ # A fresh project reads only some of the package's data, so using the wheel (next step)
145
+ # notices some missing files and not others. The lists are compared instead.
146
+ shell: bash
147
+ run: |
148
+ wheel=$(python -c "import glob, os; print(glob.glob(os.path.join(os.environ['RUNNER_TEMP'], 'wheelhouse', '*.whl'))[0])")
149
+ python .github/scripts/check_wheel_files.py "$wheel" .
150
+ - name: The wheel has a description, of a kind PyPI accepts
151
+ # publish.yml makes the same check before an upload. Made here too, a build that lost
152
+ # its description is found on the pull request that broke it, not at a release. twine
153
+ # reads the metadata and does not render Markdown: tests/test_docs.py holds the links.
154
+ shell: bash
155
+ run: |
156
+ wheel=$(python -c "import glob, os; print(glob.glob(os.path.join(os.environ['RUNNER_TEMP'], 'wheelhouse', '*.whl'))[0])")
157
+ python -m pip install twine
158
+ python -m twine check --strict "$wheel"
159
+ - name: Install it into a fresh venv and use it
160
+ shell: bash
161
+ run: |
162
+ wheel=$(python -c "import glob, os; print(glob.glob(os.path.join(os.environ['RUNNER_TEMP'], 'wheelhouse', '*.whl'))[0])")
163
+ expected=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])" | tr -d '\r')
164
+ python -m venv "$RUNNER_TEMP/wheel-venv"
165
+ if [ -d "$RUNNER_TEMP/wheel-venv/Scripts" ]; then
166
+ BIN="$RUNNER_TEMP/wheel-venv/Scripts"
167
+ else
168
+ BIN="$RUNNER_TEMP/wheel-venv/bin"
169
+ fi
170
+ "$BIN/python" -m pip install "$wheel"
171
+
172
+ # Away from the checkout, so nothing can be imported from src/.
173
+ cd "$RUNNER_TEMP"
174
+ "$BIN/python" -c "import manuscript_guard as m; assert 'site-packages' in m.__file__, m.__file__"
175
+ installed=$("$BIN/manuscript-guard" --version | tr -d '\r')
176
+ [ "$installed" = "$expected" ] || { echo "installed $installed, pyproject says $expected"; exit 1; }
177
+ "$BIN/manuscript-guard" --help > /dev/null
178
+
179
+ "$BIN/manuscript-guard" init smoke --title "Smoke test"
180
+ cd smoke
181
+ "$BIN/manuscript-guard" check
182
+ "$BIN/manuscript-guard" build --offline
183
+ test -s build/manuscript.docx
184
+
185
+ # Package data an editable install never misses: the journal template and the checklist
186
+ # recipes, asked for directly (`journal --template` refuses a project whose authors
187
+ # are still the placeholder), and a recipe found by the command that uses it
188
+ # (transcribe then fails on the missing document, which is after it found the
189
+ # recipe). The schemas were read by `check` above.
190
+ "$BIN/python" -c "from manuscript_guard.paths import SHIPPED_JOURNALS as J, SHIPPED_RECIPES as R; assert (J / 'TEMPLATE.yaml').is_file(); assert len(list(R.glob('*.recipe.yaml'))) >= 13"
191
+
192
+ # The skills, which are tracked outside the package and taken into the wheel when it
193
+ # is built. In a checkout `install-skills` reads them from plugin/skills, so only an
194
+ # installed copy shows whether the wheel has them.
195
+ "$BIN/manuscript-guard" install-skills --project
196
+ test -s .agents/skills/project-setup/SKILL.md
197
+ test -s .agents/skills/.manuscript-guard.json
198
+ # And they are the plugin's, file for file.
199
+ "$BIN/python" -c "import filecmp, os, sys; c = filecmp.dircmp(os.path.join(os.environ['GITHUB_WORKSPACE'], 'plugin', 'skills'), os.path.join('.agents', 'skills'), ignore=['.manuscript-guard.json']); bad = [c.left_only, c.right_only, c.diff_files] + [s.left_only + s.right_only + s.diff_files for s in c.subdirs.values()]; sys.exit(1 if any(bad) else 0)"
200
+ "$BIN/manuscript-guard" check
201
+ out=$("$BIN/manuscript-guard" transcribe STROBE 2>&1 || true)
202
+ [[ "$out" == *STROBE.recipe.yaml* ]] || { echo "$out"; exit 1; }
203
+ hook=$(echo '{}' | "$BIN/manuscript-guard-hook" session-start)
204
+ [[ "$hook" == *SessionStart* ]] || { echo "hook said: $hook"; exit 1; }
@@ -0,0 +1,137 @@
1
+ name: Publish to PyPI
2
+
3
+ # Every version on main is released. The maintainer raises the number on main after merges
4
+ # that change the tool (CONTRIBUTING.md), and that push is the release: this workflow tags
5
+ # the commit, makes the GitHub Release, builds, and uploads to PyPI, with nobody's click.
6
+ #
7
+ # A release on PyPI is permanent: a version can be withdrawn, and never replaced or used
8
+ # again. So a push releases only where it changed the version to one PyPI does not have
9
+ # (.github/scripts/release_decision.py). A push that leaves the number alone releases
10
+ # nothing, and neither does a second run for a version already there.
11
+ on:
12
+ push:
13
+ branches: [main]
14
+ paths: [pyproject.toml]
15
+ # To finish a release that stopped half way: run it by hand, on main.
16
+ workflow_dispatch:
17
+
18
+ permissions: {}
19
+
20
+ # One release at a time, and never a second one cancelling the first mid-upload.
21
+ concurrency:
22
+ group: publish-to-pypi
23
+ cancel-in-progress: false
24
+
25
+ jobs:
26
+ decide:
27
+ name: Is there a version to release?
28
+ if: github.ref == 'refs/heads/main'
29
+ runs-on: ubuntu-latest
30
+ timeout-minutes: 5
31
+ permissions:
32
+ contents: read
33
+ outputs:
34
+ publish: ${{ steps.decide.outputs.publish }}
35
+ version: ${{ steps.decide.outputs.version }}
36
+ steps:
37
+ - uses: actions/checkout@v6
38
+ with:
39
+ persist-credentials: false
40
+ # The whole history, so that the commit this push replaced can be read.
41
+ fetch-depth: 0
42
+ - uses: actions/setup-python@v6
43
+ with:
44
+ python-version: "3.12"
45
+ - id: decide
46
+ # The commit reaches the script through the environment, never as part of the
47
+ # command. It is empty for a run started by hand, which then asks PyPI alone.
48
+ env:
49
+ BEFORE: ${{ github.event.before }}
50
+ run: python .github/scripts/release_decision.py --before "$BEFORE" >> "$GITHUB_OUTPUT"
51
+
52
+ build:
53
+ name: Build the distributions
54
+ needs: decide
55
+ if: needs.decide.outputs.publish == 'true'
56
+ runs-on: ubuntu-latest
57
+ timeout-minutes: 15
58
+ permissions:
59
+ contents: read
60
+ steps:
61
+ - uses: actions/checkout@v6
62
+ with:
63
+ persist-credentials: false
64
+ - uses: actions/setup-python@v6
65
+ with:
66
+ python-version: "3.12"
67
+ - name: Build a source distribution, and a wheel from it
68
+ run: |
69
+ python -m pip install --upgrade pip build twine
70
+ python -m build
71
+ - name: The wheel holds every file git tracks in the package, and no others
72
+ # CI's wheel job makes this comparison on a wheel built from the checkout. The one
73
+ # uploaded is built from the source distribution, so it is compared too.
74
+ run: python .github/scripts/check_wheel_files.py dist/*.whl .
75
+ - name: Each distribution has a description, of a kind PyPI accepts
76
+ # twine reads the metadata. It does not render Markdown, so this says that a
77
+ # description and its content type are there, not that the page will look right.
78
+ run: twine check --strict dist/*
79
+ - uses: actions/upload-artifact@v5
80
+ with:
81
+ name: distributions
82
+ path: dist/
83
+ if-no-files-found: error
84
+
85
+ release:
86
+ name: Tag the commit and make the GitHub Release
87
+ needs: [decide, build]
88
+ runs-on: ubuntu-latest
89
+ timeout-minutes: 5
90
+ # The one job that may write to the repository, and it writes a tag and a release.
91
+ permissions:
92
+ contents: write
93
+ steps:
94
+ - uses: actions/checkout@v6
95
+ with:
96
+ persist-credentials: false
97
+ # With the tags, to see whether this version's is already there.
98
+ fetch-depth: 0
99
+ - name: Tag v<version> at this commit, unless it is already there
100
+ # The description PyPI shows links to files at this tag, so the tag comes before
101
+ # the upload: a release on GitHub with no upload yet is mended by running again,
102
+ # an upload whose links lead nowhere is not. A tag of this name on another commit
103
+ # stops the release: something else made it, and it is not this version.
104
+ env:
105
+ GH_TOKEN: ${{ github.token }}
106
+ TAG: v${{ needs.decide.outputs.version }}
107
+ run: |
108
+ if at=$(git rev-parse --quiet --verify "refs/tags/$TAG^{commit}"); then
109
+ if [ "$at" != "$GITHUB_SHA" ]; then
110
+ echo "::error::$TAG is already at $at, and this run builds $GITHUB_SHA"
111
+ exit 1
112
+ fi
113
+ fi
114
+ if ! gh release view "$TAG" > /dev/null 2>&1; then
115
+ gh release create "$TAG" --target "$GITHUB_SHA" --title "$TAG" --generate-notes
116
+ fi
117
+
118
+ publish:
119
+ name: Upload to PyPI
120
+ needs: [decide, build, release]
121
+ runs-on: ubuntu-latest
122
+ timeout-minutes: 10
123
+ # PyPI trusts this workflow file, in this repository, in this environment, and nothing
124
+ # else, so the upload carries no password and no token. The pairing is set on PyPI: as a
125
+ # pending publisher of the account before the first release, and in the project's
126
+ # publishing settings after it. It names this file, so renaming it breaks the upload.
127
+ environment:
128
+ name: pypi
129
+ url: https://pypi.org/project/manuscript-guard/${{ needs.decide.outputs.version }}/
130
+ permissions:
131
+ id-token: write
132
+ steps:
133
+ - uses: actions/download-artifact@v6
134
+ with:
135
+ name: distributions
136
+ path: dist/
137
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,50 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ .venv/
5
+ venv/
6
+ *.egg-info/
7
+ dist/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .mypy_cache/
11
+
12
+ # setuptools output, at the repository root only. NOT a bare `build/`: that matches any
13
+ # directory of that name at any depth, and it silently excluded the whole of
14
+ # src/manuscript_guard/build/ from the repository. Everything passed locally, where the
15
+ # files exist on disk, and CI failed on every platform with ModuleNotFoundError.
16
+ /build/
17
+
18
+ # R
19
+ .Rhistory
20
+ .RData
21
+ .Rproj.user/
22
+ renv/library/
23
+ renv/staging/
24
+
25
+ # Manuscript build artefacts (always regenerated; never a source of truth). Named
26
+ # explicitly for the same reason as above.
27
+ example/build/
28
+ *.docx
29
+ *.pdf
30
+ !profiles/**/*.pdf
31
+
32
+ # Official guideline documents, downloaded for transcription. Not committed: each carries
33
+ # its own licence, and several are CC BY-NC-SA, which cannot be redistributed from an MIT
34
+ # repository. The transcribed YAML records the source URL and licence so anyone can fetch
35
+ # the original and check the transcription.
36
+ profiles/reporting/sources/
37
+
38
+ # Generated checklist profiles. Produced locally by `manuscript-guard transcribe` from your
39
+ # own copy of each official document, so everyone gets an identical profile without the
40
+ # repository redistributing text it may not have the right to. The recipes ARE committed.
41
+ profiles/reporting/*.yaml
42
+
43
+ # OS
44
+ .DS_Store
45
+ Thumbs.db
46
+
47
+ # pytest-cov output
48
+ .coverage
49
+ .coverage.*
50
+ htmlcov/