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.
- manuscript_guard-0.2.431/.claude-plugin/marketplace.json +23 -0
- manuscript_guard-0.2.431/.gitattributes +25 -0
- manuscript_guard-0.2.431/.github/ISSUE_TEMPLATE/design-claim-is-untrue.md +12 -0
- manuscript_guard-0.2.431/.github/ISSUE_TEMPLATE/gate-got-it-wrong.md +23 -0
- manuscript_guard-0.2.431/.github/scripts/check_wheel_files.py +87 -0
- manuscript_guard-0.2.431/.github/scripts/release_decision.py +101 -0
- manuscript_guard-0.2.431/.github/workflows/ci.yml +204 -0
- manuscript_guard-0.2.431/.github/workflows/publish.yml +137 -0
- manuscript_guard-0.2.431/.gitignore +50 -0
- manuscript_guard-0.2.431/ATTRIBUTION.md +194 -0
- manuscript_guard-0.2.431/CITATION.cff +28 -0
- manuscript_guard-0.2.431/CLAUDE.md +178 -0
- manuscript_guard-0.2.431/CONTRIBUTING.md +84 -0
- manuscript_guard-0.2.431/DESIGN.md +7343 -0
- manuscript_guard-0.2.431/LICENSE +21 -0
- manuscript_guard-0.2.431/PKG-INFO +377 -0
- manuscript_guard-0.2.431/README.md +329 -0
- manuscript_guard-0.2.431/docs/agent-tools.md +190 -0
- manuscript_guard-0.2.431/docs/audit.md +39 -0
- manuscript_guard-0.2.431/docs/img/check-dark.svg +13 -0
- manuscript_guard-0.2.431/docs/img/check-light.svg +13 -0
- manuscript_guard-0.2.431/docs/img/loop-dark.svg +38 -0
- manuscript_guard-0.2.431/docs/img/loop-light.svg +38 -0
- manuscript_guard-0.2.431/docs/install.md +114 -0
- manuscript_guard-0.2.431/docs/principles.md +143 -0
- manuscript_guard-0.2.431/docs/review-panel.md +71 -0
- manuscript_guard-0.2.431/docs/stages.md +36 -0
- manuscript_guard-0.2.431/example/AGENTS.md +29 -0
- manuscript_guard-0.2.431/example/analysis/00_simulate.py +58 -0
- manuscript_guard-0.2.431/example/analysis/01_disproportionality.py +146 -0
- manuscript_guard-0.2.431/example/authors.yaml +37 -0
- manuscript_guard-0.2.431/example/data/reports.csv +4001 -0
- manuscript_guard-0.2.431/example/design/plan.md +67 -0
- manuscript_guard-0.2.431/example/figures/forest.guard.yaml +19 -0
- manuscript_guard-0.2.431/example/figures/forest.png +0 -0
- manuscript_guard-0.2.431/example/figures/forest.py +102 -0
- manuscript_guard-0.2.431/example/figures/forest.render.json +9 -0
- manuscript_guard-0.2.431/example/figures/forest.review.yaml +52 -0
- manuscript_guard-0.2.431/example/figures/forest.svg +252 -0
- manuscript_guard-0.2.431/example/literature/attested.yaml +18 -0
- manuscript_guard-0.2.431/example/literature/ledger.yaml +28 -0
- manuscript_guard-0.2.431/example/literature/references.bib +24 -0
- manuscript_guard-0.2.431/example/literature/sources/fictionalClassSignal2019.txt +13 -0
- manuscript_guard-0.2.431/example/literature/sources/fictionalHepaticCohort2021.txt +13 -0
- manuscript_guard-0.2.431/example/manuscript/main.md +119 -0
- manuscript_guard-0.2.431/example/manuscript/supplementary/S1_code_lists.md +37 -0
- manuscript_guard-0.2.431/example/methods.lock +11 -0
- manuscript_guard-0.2.431/example/paper.yaml +44 -0
- manuscript_guard-0.2.431/example/profiles/journals/demo-journal.yaml +54 -0
- manuscript_guard-0.2.431/example/profiles/reporting/DEMO-OBS.yaml +76 -0
- manuscript_guard-0.2.431/example/reporting/DEMO-OBS.yaml +39 -0
- manuscript_guard-0.2.431/example/results/01_disproportionality.json +430 -0
- manuscript_guard-0.2.431/example/results/01_disproportionality.json.sha256 +1 -0
- manuscript_guard-0.2.431/example/review/panel-1.yaml +29 -0
- manuscript_guard-0.2.431/example/review/panel-2.yaml +27 -0
- manuscript_guard-0.2.431/example/review/round-1/biostatistician.yaml +38 -0
- manuscript_guard-0.2.431/example/review/round-1/checklist-auditor.yaml +33 -0
- manuscript_guard-0.2.431/example/review/round-1/pharmacoepidemiologist.yaml +52 -0
- manuscript_guard-0.2.431/example/review/round-2/clinical-reader.yaml +36 -0
- manuscript_guard-0.2.431/example/review/round-2/desk-editor.yaml +29 -0
- manuscript_guard-0.2.431/plugin/.claude-plugin/plugin.json +18 -0
- manuscript_guard-0.2.431/plugin/hooks/hooks.json +53 -0
- manuscript_guard-0.2.431/plugin/skills/analysis-plan/SKILL.md +106 -0
- manuscript_guard-0.2.431/plugin/skills/figure-review/SKILL.md +102 -0
- manuscript_guard-0.2.431/plugin/skills/journal-profile/SKILL.md +141 -0
- manuscript_guard-0.2.431/plugin/skills/literature-verify/SKILL.md +121 -0
- manuscript_guard-0.2.431/plugin/skills/manuscript-writing/SKILL.md +205 -0
- manuscript_guard-0.2.431/plugin/skills/methods-writer/SKILL.md +88 -0
- manuscript_guard-0.2.431/plugin/skills/paper-audit/SKILL.md +186 -0
- manuscript_guard-0.2.431/plugin/skills/project-setup/SKILL.md +196 -0
- manuscript_guard-0.2.431/plugin/skills/reporting-checklist/SKILL.md +102 -0
- manuscript_guard-0.2.431/plugin/skills/results-binding/SKILL.md +203 -0
- manuscript_guard-0.2.431/plugin/skills/review-panel/SKILL.md +202 -0
- manuscript_guard-0.2.431/plugin/skills/reviewer-response/SKILL.md +189 -0
- manuscript_guard-0.2.431/plugin/skills/submission-pack/SKILL.md +77 -0
- manuscript_guard-0.2.431/plugin/skills/word-roundtrip/SKILL.md +342 -0
- manuscript_guard-0.2.431/pyproject.toml +119 -0
- manuscript_guard-0.2.431/r/manuscriptguard/DESCRIPTION +16 -0
- manuscript_guard-0.2.431/r/manuscriptguard/LICENSE +2 -0
- manuscript_guard-0.2.431/r/manuscriptguard/NAMESPACE +4 -0
- manuscript_guard-0.2.431/r/manuscriptguard/R/emit.R +735 -0
- manuscript_guard-0.2.431/src/manuscript_guard/__init__.py +13 -0
- manuscript_guard-0.2.431/src/manuscript_guard/annotate.py +744 -0
- manuscript_guard-0.2.431/src/manuscript_guard/audit.py +912 -0
- manuscript_guard-0.2.431/src/manuscript_guard/binding.py +196 -0
- manuscript_guard-0.2.431/src/manuscript_guard/build/__init__.py +48 -0
- manuscript_guard-0.2.431/src/manuscript_guard/build/assemble.py +359 -0
- manuscript_guard-0.2.431/src/manuscript_guard/build/document.py +783 -0
- manuscript_guard-0.2.431/src/manuscript_guard/build/figure_caption.lua +81 -0
- manuscript_guard-0.2.431/src/manuscript_guard/build/reading.py +755 -0
- manuscript_guard-0.2.431/src/manuscript_guard/build/styles.py +129 -0
- manuscript_guard-0.2.431/src/manuscript_guard/build/submission.py +481 -0
- manuscript_guard-0.2.431/src/manuscript_guard/build/tex_check.py +205 -0
- manuscript_guard-0.2.431/src/manuscript_guard/build/zotero_word.lua +100 -0
- manuscript_guard-0.2.431/src/manuscript_guard/classify.py +557 -0
- manuscript_guard-0.2.431/src/manuscript_guard/cli.py +2481 -0
- manuscript_guard-0.2.431/src/manuscript_guard/contracts/__init__.py +62 -0
- manuscript_guard-0.2.431/src/manuscript_guard/contracts/_schema.py +166 -0
- manuscript_guard-0.2.431/src/manuscript_guard/contracts/literature.py +122 -0
- manuscript_guard-0.2.431/src/manuscript_guard/contracts/project.py +701 -0
- manuscript_guard-0.2.431/src/manuscript_guard/contracts/results.py +215 -0
- manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/attested.schema.json +52 -0
- manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/authors.schema.json +95 -0
- manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/figure_review.schema.json +65 -0
- manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/journal.schema.json +95 -0
- manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/ledger.schema.json +57 -0
- manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/panel.schema.json +61 -0
- manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/paper.schema.json +163 -0
- manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/reporting.schema.json +51 -0
- manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/reporting_completion.schema.json +33 -0
- manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/results.schema.json +207 -0
- manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/review.schema.json +129 -0
- manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/review_reply.schema.json +59 -0
- manuscript_guard-0.2.431/src/manuscript_guard/contracts/schemas/revision.schema.json +93 -0
- manuscript_guard-0.2.431/src/manuscript_guard/contracts/values.py +263 -0
- manuscript_guard-0.2.431/src/manuscript_guard/data/abbreviations.yaml +56 -0
- manuscript_guard-0.2.431/src/manuscript_guard/data/ai_writing.yaml +190 -0
- manuscript_guard-0.2.431/src/manuscript_guard/data/conventions.yaml +133 -0
- manuscript_guard-0.2.431/src/manuscript_guard/data/plot_params.yaml +179 -0
- manuscript_guard-0.2.431/src/manuscript_guard/data/spelling_variants.tsv +8436 -0
- manuscript_guard-0.2.431/src/manuscript_guard/data/structural.yaml +262 -0
- manuscript_guard-0.2.431/src/manuscript_guard/data/terms.yaml +203 -0
- manuscript_guard-0.2.431/src/manuscript_guard/docxtext.py +909 -0
- manuscript_guard-0.2.431/src/manuscript_guard/emit.py +827 -0
- manuscript_guard-0.2.431/src/manuscript_guard/findings.py +143 -0
- manuscript_guard-0.2.431/src/manuscript_guard/gates/__init__.py +81 -0
- manuscript_guard-0.2.431/src/manuscript_guard/gates/citations.py +215 -0
- manuscript_guard-0.2.431/src/manuscript_guard/gates/consistency.py +135 -0
- manuscript_guard-0.2.431/src/manuscript_guard/gates/design.py +185 -0
- manuscript_guard-0.2.431/src/manuscript_guard/gates/figure_review.py +171 -0
- manuscript_guard-0.2.431/src/manuscript_guard/gates/figure_source.py +310 -0
- manuscript_guard-0.2.431/src/manuscript_guard/gates/figures.py +408 -0
- manuscript_guard-0.2.431/src/manuscript_guard/gates/freshness.py +253 -0
- manuscript_guard-0.2.431/src/manuscript_guard/gates/journal.py +277 -0
- manuscript_guard-0.2.431/src/manuscript_guard/gates/language.py +848 -0
- manuscript_guard-0.2.431/src/manuscript_guard/gates/literature.py +176 -0
- manuscript_guard-0.2.431/src/manuscript_guard/gates/methods.py +202 -0
- manuscript_guard-0.2.431/src/manuscript_guard/gates/numbers.py +847 -0
- manuscript_guard-0.2.431/src/manuscript_guard/gates/reporting.py +344 -0
- manuscript_guard-0.2.431/src/manuscript_guard/gates/review.py +821 -0
- manuscript_guard-0.2.431/src/manuscript_guard/gates/revision.py +334 -0
- manuscript_guard-0.2.431/src/manuscript_guard/gates/spelling.py +279 -0
- manuscript_guard-0.2.431/src/manuscript_guard/gates/vocabulary.py +334 -0
- manuscript_guard-0.2.431/src/manuscript_guard/gates/writing.py +265 -0
- manuscript_guard-0.2.431/src/manuscript_guard/hooks.py +778 -0
- manuscript_guard-0.2.431/src/manuscript_guard/literature/__init__.py +19 -0
- manuscript_guard-0.2.431/src/manuscript_guard/literature/sources.py +181 -0
- manuscript_guard-0.2.431/src/manuscript_guard/merge.py +2433 -0
- manuscript_guard-0.2.431/src/manuscript_guard/panel/__init__.py +17 -0
- manuscript_guard-0.2.431/src/manuscript_guard/panel/client.py +513 -0
- manuscript_guard-0.2.431/src/manuscript_guard/panel/commands.py +331 -0
- manuscript_guard-0.2.431/src/manuscript_guard/panel/plan.py +535 -0
- manuscript_guard-0.2.431/src/manuscript_guard/panel/prompt.py +368 -0
- manuscript_guard-0.2.431/src/manuscript_guard/panel/providers.py +350 -0
- manuscript_guard-0.2.431/src/manuscript_guard/panel/reply.py +119 -0
- manuscript_guard-0.2.431/src/manuscript_guard/panel/run.py +578 -0
- manuscript_guard-0.2.431/src/manuscript_guard/paths.py +88 -0
- manuscript_guard-0.2.431/src/manuscript_guard/policy.py +219 -0
- manuscript_guard-0.2.431/src/manuscript_guard/profiles/journals/TEMPLATE.yaml +210 -0
- manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/ARRIVE-2.0.recipe.yaml +33 -0
- manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/CONSORT.recipe.yaml +25 -0
- manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/PRISMA-2020-abstracts.recipe.yaml +21 -0
- manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/PRISMA-2020.recipe.yaml +20 -0
- manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/READUS-PV-abstracts.recipe.yaml +19 -0
- manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/READUS-PV.recipe.yaml +24 -0
- manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/RECORD-PE.recipe.yaml +26 -0
- manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/RECORD.recipe.yaml +26 -0
- manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/SPIRIT-2025.recipe.yaml +20 -0
- manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/STROBE.recipe.yaml +29 -0
- manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/TRIPOD-development-validation.recipe.yaml +23 -0
- manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/TRIPOD-development.recipe.yaml +24 -0
- manuscript_guard-0.2.431/src/manuscript_guard/profiles/reporting/recipes/TRIPOD-validation.recipe.yaml +20 -0
- manuscript_guard-0.2.431/src/manuscript_guard/record.py +402 -0
- manuscript_guard-0.2.431/src/manuscript_guard/render.py +125 -0
- manuscript_guard-0.2.431/src/manuscript_guard/reporting/__init__.py +24 -0
- manuscript_guard-0.2.431/src/manuscript_guard/reporting/build.py +138 -0
- manuscript_guard-0.2.431/src/manuscript_guard/reporting/columns.py +174 -0
- manuscript_guard-0.2.431/src/manuscript_guard/reporting/fetch.py +131 -0
- manuscript_guard-0.2.431/src/manuscript_guard/reporting/transcribe.py +288 -0
- manuscript_guard-0.2.431/src/manuscript_guard/roundtrip.py +3277 -0
- manuscript_guard-0.2.431/src/manuscript_guard/safexml.py +106 -0
- manuscript_guard-0.2.431/src/manuscript_guard/scaffold.py +370 -0
- manuscript_guard-0.2.431/src/manuscript_guard/skillcopy.py +338 -0
- manuscript_guard-0.2.431/src/manuscript_guard/tables.py +255 -0
- manuscript_guard-0.2.431/src/manuscript_guard/text/__init__.py +17 -0
- manuscript_guard-0.2.431/src/manuscript_guard/text/attributes.py +105 -0
- manuscript_guard-0.2.431/src/manuscript_guard/text/blocks.py +1075 -0
- manuscript_guard-0.2.431/src/manuscript_guard/text/code.py +245 -0
- manuscript_guard-0.2.431/src/manuscript_guard/text/comments.py +368 -0
- manuscript_guard-0.2.431/src/manuscript_guard/text/docx.py +371 -0
- manuscript_guard-0.2.431/src/manuscript_guard/text/fences.py +540 -0
- manuscript_guard-0.2.431/src/manuscript_guard/text/inline.py +318 -0
- manuscript_guard-0.2.431/src/manuscript_guard/text/masking.py +775 -0
- manuscript_guard-0.2.431/src/manuscript_guard/text/placeholders.py +123 -0
- manuscript_guard-0.2.431/src/manuscript_guard/text/sections.py +629 -0
- manuscript_guard-0.2.431/src/manuscript_guard/text/tex.py +478 -0
- manuscript_guard-0.2.431/src/manuscript_guard/text/tokens.py +197 -0
- manuscript_guard-0.2.431/src/manuscript_guard/verify.py +464 -0
- manuscript_guard-0.2.431/src/manuscript_guard/wordfonts.py +365 -0
- manuscript_guard-0.2.431/src/manuscript_guard/zotero/__init__.py +30 -0
- manuscript_guard-0.2.431/src/manuscript_guard/zotero/citations.py +129 -0
- manuscript_guard-0.2.431/src/manuscript_guard/zotero/client.py +270 -0
- manuscript_guard-0.2.431/tests/conftest.py +242 -0
- manuscript_guard-0.2.431/tests/data/exemptions.yaml +134 -0
- manuscript_guard-0.2.431/tests/data/rule_cases.yaml +423 -0
- manuscript_guard-0.2.431/tests/data/timing_budgets.yaml +206 -0
- manuscript_guard-0.2.431/tests/test_annotate.py +292 -0
- manuscript_guard-0.2.431/tests/test_artifact.py +290 -0
- manuscript_guard-0.2.431/tests/test_audit.py +1344 -0
- manuscript_guard-0.2.431/tests/test_binding.py +246 -0
- manuscript_guard-0.2.431/tests/test_build.py +665 -0
- manuscript_guard-0.2.431/tests/test_ci_requirements.py +138 -0
- manuscript_guard-0.2.431/tests/test_cli.py +1268 -0
- manuscript_guard-0.2.431/tests/test_comments.py +228 -0
- manuscript_guard-0.2.431/tests/test_compliance.py +524 -0
- manuscript_guard-0.2.431/tests/test_contracts.py +1149 -0
- manuscript_guard-0.2.431/tests/test_corruption.py +9443 -0
- manuscript_guard-0.2.431/tests/test_docs.py +261 -0
- manuscript_guard-0.2.431/tests/test_emit.py +739 -0
- manuscript_guard-0.2.431/tests/test_exemptions.py +156 -0
- manuscript_guard-0.2.431/tests/test_figures.py +409 -0
- manuscript_guard-0.2.431/tests/test_hooks.py +2061 -0
- manuscript_guard-0.2.431/tests/test_input_digests.py +279 -0
- manuscript_guard-0.2.431/tests/test_interaction.py +138 -0
- manuscript_guard-0.2.431/tests/test_language.py +926 -0
- manuscript_guard-0.2.431/tests/test_lifecycle.py +251 -0
- manuscript_guard-0.2.431/tests/test_literature.py +476 -0
- manuscript_guard-0.2.431/tests/test_methods.py +172 -0
- manuscript_guard-0.2.431/tests/test_no_duplicate_names.py +234 -0
- manuscript_guard-0.2.431/tests/test_ordinary_sessions.py +890 -0
- manuscript_guard-0.2.431/tests/test_packaging.py +278 -0
- manuscript_guard-0.2.431/tests/test_pandoc_agreement.py +1949 -0
- manuscript_guard-0.2.431/tests/test_parity.py +138 -0
- manuscript_guard-0.2.431/tests/test_plugin.py +614 -0
- manuscript_guard-0.2.431/tests/test_policy.py +471 -0
- manuscript_guard-0.2.431/tests/test_printed_record.py +349 -0
- manuscript_guard-0.2.431/tests/test_r_emitter.py +461 -0
- manuscript_guard-0.2.431/tests/test_raw_tex.py +493 -0
- manuscript_guard-0.2.431/tests/test_record.py +790 -0
- manuscript_guard-0.2.431/tests/test_review.py +1225 -0
- manuscript_guard-0.2.431/tests/test_review_plan.py +934 -0
- manuscript_guard-0.2.431/tests/test_review_providers.py +1023 -0
- manuscript_guard-0.2.431/tests/test_review_run.py +1316 -0
- manuscript_guard-0.2.431/tests/test_review_transport.py +245 -0
- manuscript_guard-0.2.431/tests/test_revision.py +380 -0
- manuscript_guard-0.2.431/tests/test_robustness.py +1057 -0
- manuscript_guard-0.2.431/tests/test_round_five.py +425 -0
- manuscript_guard-0.2.431/tests/test_roundtrip.py +12915 -0
- manuscript_guard-0.2.431/tests/test_rules.py +179 -0
- manuscript_guard-0.2.431/tests/test_scaffold.py +452 -0
- manuscript_guard-0.2.431/tests/test_script_signs.py +564 -0
- manuscript_guard-0.2.431/tests/test_seed_revision.py +155 -0
- manuscript_guard-0.2.431/tests/test_skillcopy.py +751 -0
- manuscript_guard-0.2.431/tests/test_spelling.py +1064 -0
- manuscript_guard-0.2.431/tests/test_submission.py +426 -0
- manuscript_guard-0.2.431/tests/test_tex.py +723 -0
- manuscript_guard-0.2.431/tests/test_tex_in_check.py +763 -0
- manuscript_guard-0.2.431/tests/test_text.py +710 -0
- manuscript_guard-0.2.431/tests/test_timing_budgets.py +739 -0
- manuscript_guard-0.2.431/tests/test_transcribe.py +753 -0
- manuscript_guard-0.2.431/tests/test_verify.py +264 -0
- manuscript_guard-0.2.431/tests/test_version.py +64 -0
- manuscript_guard-0.2.431/tests/test_vocabulary.py +772 -0
- manuscript_guard-0.2.431/tests/test_writing.py +259 -0
- manuscript_guard-0.2.431/tests/test_zotero.py +331 -0
- manuscript_guard-0.2.431/tests/test_zotero_word.py +184 -0
- manuscript_guard-0.2.431/tools/derive_spelling_variants.py +393 -0
- 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/
|