skilldrop-cli 1.11.1 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/catalogue.json +2 -1
- package/model-routing.json +4 -0
- package/package.json +1 -1
- package/packs/research/skills/manuscript-check/SKILL.md +121 -0
- package/packs/research/skills/manuscript-check/evals/eval_queries.json +30 -0
- package/packs/research/skills/manuscript-check/evals/evals.json +73 -0
- package/packs/research/skills/manuscript-check/evals/files/1/paper.md +56 -0
- package/packs/research/skills/manuscript-check/evals/files/2/paper.md +118 -0
- package/packs/research/skills/manuscript-check/evals/files/3/report.md +115 -0
- package/packs/research/skills/manuscript-check/evals/files/4/paper.md +79 -0
- package/packs/research/skills/manuscript-check/evals/files/5/chapter3.md +65 -0
- package/packs/research/skills/manuscript-check/examples/inputs/paper.md +56 -0
- package/packs/research/skills/manuscript-check/examples/retention-paper.md +48 -0
- package/packs/research/skills/manuscript-check/manifest.json +36 -0
- package/packs/research/skills/manuscript-check/scripts/manuscript_check.py +355 -0
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ Install one pack. Every role pack also brings `core` (intake, critique, review c
|
|
|
65
65
|
- **Building on the Claude API:** `api-builder` — prompt caching, token budgets, eval generation, tool schemas. [Open pack](https://sananthanarayan.github.io/skilldrop/packs/api-builder/)
|
|
66
66
|
- **Moving between formats:** `converters` — Markdown to Word, Excel and HTML, files back to Markdown, and Mermaid diagrams checked and rendered. [Open pack](https://sananthanarayan.github.io/skilldrop/packs/converters/)
|
|
67
67
|
- **Designing the experience:** `experience-design` — information architecture, UX writing, content design, component specs, service blueprints. [Open pack](https://sananthanarayan.github.io/skilldrop/packs/experience-design/)
|
|
68
|
-
- **Researching a question:** `research` — research plans, cited source synthesis, competing hypotheses. [Open pack](https://sananthanarayan.github.io/skilldrop/packs/research/)
|
|
68
|
+
- **Researching a question:** `research` — research plans, cited source synthesis, competing hypotheses, and a manuscript's citations and cross-references checked before submission. [Open pack](https://sananthanarayan.github.io/skilldrop/packs/research/)
|
|
69
69
|
- **Keeping the tracker straight:** `trackers` — backlog triage, weekly status from tracker data, briefs turned into issues. [Open pack](https://sananthanarayan.github.io/skilldrop/packs/trackers/)
|
|
70
70
|
- **Data and analytics:** `data-analytics` — metric definitions, SQL review, dashboard specs. [Open pack](https://sananthanarayan.github.io/skilldrop/packs/data-analytics/)
|
|
71
71
|
- **Governance, risk and compliance:** `grc` — DPIAs, SOC 2 evidence maps, risk registers, security questionnaire answers. [Open pack](https://sananthanarayan.github.io/skilldrop/packs/grc/)
|
package/catalogue.json
CHANGED
package/model-routing.json
CHANGED
|
@@ -363,6 +363,10 @@
|
|
|
363
363
|
"tier": "light",
|
|
364
364
|
"rationale": "Measured (RFC-0073): on its held-out evals the light tier met 71% and 71% of checks in two tier trials, against standard's 71% and heavy's 71% and 71%, within 5 points of the best both times."
|
|
365
365
|
},
|
|
366
|
+
"manuscript-check": {
|
|
367
|
+
"tier": "standard",
|
|
368
|
+
"rationale": "Citation and cross-reference matching come from the script; reading the reference list for style consistency is bounded."
|
|
369
|
+
},
|
|
366
370
|
"mcp-pairing": {
|
|
367
371
|
"tier": "standard",
|
|
368
372
|
"rationale": "Matching a loop's stages to the context each needs and the narrowest access that provides it is bounded judgment over a short checklist."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skilldrop-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "Portable AI-agent skills, measured against the agent without them. ADRs, PRDs, runbooks, decks and reviews, installable into Claude Code, Cursor, Kiro, and any IDE that takes custom instructions. One command per skill or per role-based pack.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Sanjay Ananthanarayan",
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: manuscript-check
|
|
3
|
+
description: Check a paper, thesis chapter or report before it goes to a journal, a supervisor, examiners or a publisher — a script reads the manuscript and finds every citation with no reference (including near misses: the right author with another year, a misspelt name), references never cited, the same work listed twice, numbered references out of citation order, and Figure, Table and Section references to things that don't exist or are never mentioned, plus footnotes and the abstract's length — then reads the reference list for consistency of style. Use when someone asks to "check my references", "check my citations match the bibliography", "proofread my paper before submission", "check my figure and table numbering", "is my thesis chapter ready to send", or "check the cross-references in this report".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# manuscript-check
|
|
7
|
+
|
|
8
|
+
Reviewers and examiners notice a citation with no reference, "see Figure 3" in a paper with two
|
|
9
|
+
figures, and the same source listed twice, and each one costs credibility. These faults come from
|
|
10
|
+
revising: a figure cut, a section renumbered, a reference updated to its published year in the
|
|
11
|
+
list but not in the text. A capable agent reads a manuscript fluently and misses them, because
|
|
12
|
+
it reads what the author meant. A script reads every citation against every reference both
|
|
13
|
+
ways, and every figure, table and section reference against what exists. The agent then reads
|
|
14
|
+
the reference list for what a script can't judge.
|
|
15
|
+
|
|
16
|
+
## How to respond
|
|
17
|
+
|
|
18
|
+
**These rules come before the steps and outrank them:**
|
|
19
|
+
|
|
20
|
+
- **Lead with what a reviewer or examiner would catch, in plain words.** ✅ *"Six things to fix:
|
|
21
|
+
'Kuh et al., 2009' is 2008 in your list; 'Yeager (2016)' is spelt Yeagar there; Rosenbaum and
|
|
22
|
+
Rubin (1983) is listed twice; the text refers to Section 6, Table 2 and Figure 3, which don't
|
|
23
|
+
exist."* Never mention a script, this skill's name, a path inside its folder or a temporary
|
|
24
|
+
path, and never call your work "the check" or "a tool".
|
|
25
|
+
- **Every citation and cross-reference finding comes from the manuscript,** with its line. Never
|
|
26
|
+
claim a reference is missing from a read alone.
|
|
27
|
+
- **Say which side is wrong when it's clear,** and when it isn't, give both fixes: a 2009
|
|
28
|
+
citation beside a 2008 entry is usually the citation, but a paper that went from preprint to
|
|
29
|
+
print can be the list. Never delete a reference unasked: an uncited entry may need a citation
|
|
30
|
+
added rather than removing.
|
|
31
|
+
- **Then read the manuscript for internal consistency, as a reviewer does:** every number in
|
|
32
|
+
the abstract and the text against the tables and results it comes from (a total, a mean, a
|
|
33
|
+
percentage, a sample size); every claim in the abstract present in the results; every
|
|
34
|
+
analysis in the results described in the methods; captions in one format. Work the
|
|
35
|
+
arithmetic where you can. One line each, with the line number and the correction.
|
|
36
|
+
- **Line numbers come from the check's output,** never counted by eye, and every fix in the reply
|
|
37
|
+
agrees with every other: no advice to cite a source in one item that another item says doesn't
|
|
38
|
+
support the claim.
|
|
39
|
+
- **When numbered references are out of citation order, give the whole renumbering** from the
|
|
40
|
+
check's old-to-new map, so the author can apply it in one pass.
|
|
41
|
+
- **Then read the reference list for consistency** in the style the manuscript uses (APA,
|
|
42
|
+
Harvard, Vancouver, IEEE, Chicago): author formats, "et al." thresholds, italics, missing
|
|
43
|
+
volumes, pages or DOIs, a preprint cited where the published version exists, in-text forms
|
|
44
|
+
that mix styles. One line each, with the corrected form for one example.
|
|
45
|
+
- **Their file is never overwritten.** A corrected copy goes beside it under a plain name
|
|
46
|
+
(`paper-corrected.docx`), only when asked, with every change listed. Working files go in a
|
|
47
|
+
private temporary folder (`mktemp -d`) and are deleted before replying.
|
|
48
|
+
|
|
49
|
+
1. **Find the manuscript and its limits:** the target journal or the regulations (abstract
|
|
50
|
+
length, reference style), when the person names them. Ask at most once.
|
|
51
|
+
|
|
52
|
+
2. **Run the check.** It reads Markdown, text and Word; for LaTeX or PDF, save as text first.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Claude Code
|
|
56
|
+
python3 "${CLAUDE_SKILL_DIR}/scripts/manuscript_check.py" ./paper.docx --abstract-limit 250
|
|
57
|
+
# Other IDEs (from the skill folder)
|
|
58
|
+
python3 scripts/manuscript_check.py ./chapter-3.md
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Exit 0 means no failures, 1 means a failure, 2 means the file can't be read. It finds the
|
|
62
|
+
reference list by its heading (References, Bibliography, Works Cited) and tells numbered from
|
|
63
|
+
author-year citations itself. It **fails** on a citation with no reference, the same work
|
|
64
|
+
listed twice, a reference to a figure, table or section that doesn't exist, a footnote used
|
|
65
|
+
but not defined, and an abstract over its limit. It **warns** on references never cited,
|
|
66
|
+
numbered references out of citation order, captions never referred to, numbering gaps, and
|
|
67
|
+
malformed DOIs.
|
|
68
|
+
|
|
69
|
+
3. **Read the reference list** for style consistency, as in the rules above, and check each
|
|
70
|
+
near miss against the entry it points to.
|
|
71
|
+
|
|
72
|
+
4. **Reply in this order:**
|
|
73
|
+
1. **What to fix,** most serious first, each with its line and the correction.
|
|
74
|
+
2. **Citations and references:** missing, near misses, never cited, duplicates.
|
|
75
|
+
3. **Figures, tables and sections:** references that don't exist, captions never mentioned,
|
|
76
|
+
numbering, and the full renumbering when references are out of order.
|
|
77
|
+
4. **Numbers and claims that don't agree** between the abstract, text, tables and methods.
|
|
78
|
+
5. **Style consistency in the reference list,** one line each with one corrected example.
|
|
79
|
+
6. **Corrected file,** when asked, with every change listed.
|
|
80
|
+
|
|
81
|
+
**Non-interactive runs** (subagent, CI, headless): with no reference list found, say so and
|
|
82
|
+
check the cross-references only. With no manuscript, emit `BLOCKED: need the manuscript (as
|
|
83
|
+
.docx, .md or .txt)`.
|
|
84
|
+
|
|
85
|
+
## Useful references in this skill
|
|
86
|
+
|
|
87
|
+
- [`scripts/manuscript_check.py`](scripts/manuscript_check.py) — numbered and author-year citations both ways, near misses, duplicates, figure, table and section references, footnotes, DOIs, abstract length
|
|
88
|
+
- [`examples/retention-paper.md`](examples/retention-paper.md) — worked example: a paper with a misspelt citation, a wrong year, a duplicated reference and three references to things that were cut
|
|
89
|
+
|
|
90
|
+
## Quality bar
|
|
91
|
+
|
|
92
|
+
- **Every citation is checked against the reference list, and every reference against the citations.**
|
|
93
|
+
- **A near miss names the entry it nearly matches,** so the fix is one edit.
|
|
94
|
+
- **Every Figure, Table and Section reference points at something that exists,** and every caption is mentioned.
|
|
95
|
+
- **The reply leads with what a reviewer would catch,** with lines, and no mention of scripts or files.
|
|
96
|
+
- **Nothing is deleted unasked,** and the likelier side of each mismatch is named.
|
|
97
|
+
- **Their file is never overwritten,** and every change in a corrected copy is listed.
|
|
98
|
+
|
|
99
|
+
## When to use this skill
|
|
100
|
+
|
|
101
|
+
- ✅ A paper before journal submission, or a revision before resubmission
|
|
102
|
+
- ✅ A thesis chapter before it goes to a supervisor or examiners
|
|
103
|
+
- ✅ A report or white paper with numbered figures and a reference list
|
|
104
|
+
- ✅ After cutting or renumbering figures, tables or sections
|
|
105
|
+
|
|
106
|
+
## When NOT to use this skill
|
|
107
|
+
|
|
108
|
+
- ❌ Whether a source supports the claim it's cited for: that is a reading of the sources (`source-synthesis` reads sources for a question)
|
|
109
|
+
- ❌ Writing a literature review: plan it with `research-plan` and draft it from the sources
|
|
110
|
+
- ❌ Plagiarism or similarity checking
|
|
111
|
+
- ❌ Copy-editing the prose itself
|
|
112
|
+
|
|
113
|
+
## Anti-patterns to avoid
|
|
114
|
+
|
|
115
|
+
- ❌ **"The references look complete."** Check both ways: a list entry never cited is as visible to a reviewer as a missing one.
|
|
116
|
+
- ❌ **Deleting uncited references** without asking: some need a citation added instead.
|
|
117
|
+
- ❌ **Missing the near miss.** "Smith (2019)" against "Smith (2020)" in the list is one reference with the wrong year, not two problems.
|
|
118
|
+
- ❌ **Renumbering every figure** to fix one gap without listing what moved.
|
|
119
|
+
- ❌ **Reformatting the whole list into a different style** unasked.
|
|
120
|
+
- ❌ **"The script found…"** They want the list of fixes.
|
|
121
|
+
- ❌ **Checking the references and stopping.** A total in the text that disagrees with its table is the next thing a reviewer finds.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"query": "Check my citations match my bibliography",
|
|
4
|
+
"should_trigger": true
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
"query": "Proofread my paper's references and figure numbering before submission",
|
|
8
|
+
"should_trigger": true
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"query": "Is my thesis chapter ready to send to my supervisor? Check the cross-references",
|
|
12
|
+
"should_trigger": true
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"query": "Check every figure and table in this report is referred to",
|
|
16
|
+
"should_trigger": true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"query": "Synthesise these ten papers into a literature review",
|
|
20
|
+
"should_trigger": false
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"query": "Plan a research study on remote work and wellbeing",
|
|
24
|
+
"should_trigger": false
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"query": "Critique this design document's structure",
|
|
28
|
+
"should_trigger": false
|
|
29
|
+
}
|
|
30
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "manuscript-check",
|
|
3
|
+
"evals": [
|
|
4
|
+
{
|
|
5
|
+
"id": 1,
|
|
6
|
+
"prompt": "I'm submitting paper.md to the journal next week. Can you check my citations and cross-references? It's APA 7th and the abstract limit is 50 words.",
|
|
7
|
+
"assertions": [
|
|
8
|
+
"Flags 'Kuh et al., 2009' (line 34) against Kuh et al. 2008 in the list",
|
|
9
|
+
"Flags 'Yeager (2016)' against the list's spelling 'Yeagar'",
|
|
10
|
+
"Flags Rosenbaum and Rubin (1983) as listed twice",
|
|
11
|
+
"Flags references to Section 6, Table 2 and Figure 3, which don't exist, and Figure 2 never being referred to",
|
|
12
|
+
"Flags Pascarella and Terenzini (2005) as never cited without deleting it unasked",
|
|
13
|
+
"Does not mention a script or the skill's name, and does not invent references or errors not in the file"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": 2,
|
|
18
|
+
"held_out": "2026-11-09",
|
|
19
|
+
"prompt": "Can you check my references and figure numbering before I send this to the journal? It's paper.md, APA style.",
|
|
20
|
+
"assertions": [
|
|
21
|
+
"Flags (Okonkwo-Varga, 2019) in the Introduction as not matching the list, which has Okonkwo-Varga, J. (2018), and treats it as a near miss whose likely fix is correcting the in-text year to 2018 (or confirming which year is right), not as a missing reference",
|
|
22
|
+
"Flags 'Lindqvst & Moreau, 2020' (cited twice, in the Introduction and the Discussion) as a misspelling of Lindqvist, E., & Moreau, C. (2020) in the reference list, and suggests correcting the in-text spelling",
|
|
23
|
+
"Flags (Tamberlane, 2021) in the Introduction as a citation with no matching entry in the reference list, and says a reference must be added or the citation removed",
|
|
24
|
+
"Flags Feroz, A. (2017) as listed in the references but never cited in the text",
|
|
25
|
+
"Flags the reference to Figure 3 in the Results as pointing to a figure that does not exist (only Figures 1 and 2 have captions)",
|
|
26
|
+
"Notes the reference-list style inconsistency in the Delacroix-Hume and Petrov (2021) entry: full first names ('Brigitte', 'Stefan') instead of initials, unlike every other entry",
|
|
27
|
+
"Does not invent faults or references that are not in paper.md - for example it does not report Table 1, Figure 1, Figure 2, Vasquez-Tolland (2020) or Haldane-Ruiz et al. (2016) as problems, and does not make up a full reference for Tamberlane"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": 3,
|
|
32
|
+
"held_out": "2026-11-09",
|
|
33
|
+
"prompt": "This is my pilot report for the Fernhollow Farm Trust board (report.md). I'm also going to cut it down and send it to a small journal that uses Vancouver referencing, so I want the version I have now to be right first. Could you check that every citation matches the reference list and the references are numbered properly, that all the figure, table and section cross-references work, and anything else that would embarrass me in front of the board or an editor? Please tell me exactly where each problem is and what you'd change.",
|
|
34
|
+
"assertions": [
|
|
35
|
+
"Finds that the references are not numbered in order of first citation: reference [4] is first cited (Introduction, second paragraph) before reference [3], so in Vancouver style the two should be swapped and renumbered",
|
|
36
|
+
"Finds that reference 11 is a duplicate of reference 5 (Castellanos and Whitcombe, 2022, same title and journal), and says to delete 11 and cite [5] in section 4.1 instead, renumbering what follows",
|
|
37
|
+
"Finds that reference 9 (Okafor-Brandt, 2023) is never cited in the text, so it should be cited or removed",
|
|
38
|
+
"Finds that section 3.1 refers to 'Section 4.3', which does not exist (section 4 has only 4.1 and 4.2)",
|
|
39
|
+
"Finds that Figure 2 has a caption but is never referred to in the text, and suggests where to cite it (e.g. in section 3 or 4.2 on watering)",
|
|
40
|
+
"Notes that reference 7 (Delahaye and Strand) is written in APA style (comma-separated initials with full stops, '&', year in brackets after the authors, full journal name) unlike the Vancouver format of every other entry, and shows the Vancouver form",
|
|
41
|
+
"Does not invent problems that are not in report.md - for example it does not report Figure 1, Table 1 or references 1, 2, 6, 8, 10 or 12 as missing or uncited, and does not claim any citation lacks a reference entry"
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": 4,
|
|
46
|
+
"held_out": "2026-11-09",
|
|
47
|
+
"prompt": "Check paper.md before I send it to the journal - references and cross-references, please.",
|
|
48
|
+
"assertions": [
|
|
49
|
+
"Flags 'Halvorsen, 2017' (Introduction) as a near miss for the reference Halvorsen (2019), and says the likelier fix is to correct the in-text year to 2019",
|
|
50
|
+
"Flags 'Pemberton (2017)' (Introduction) as a near miss for the reference 'Pembleton, A. (2017)' and asks which spelling is right rather than treating it as two different works",
|
|
51
|
+
"Flags 'Varga, 2021' (Discussion) as cited with no entry in the reference list and asks for the details, without inventing a reference for it",
|
|
52
|
+
"Flags 'Lindqvist, E. (2016)' as listed in References but never cited, to be cited or removed",
|
|
53
|
+
"Flags the Results' reference to 'Figure 3': only Figures 1 and 2 exist (captions after Table 1), so either a figure is missing or the reference should point to Figure 2",
|
|
54
|
+
"Flags that the abstract's 'raised mean soil moisture by 23%' disagrees with Table 1: wood chip 15.1% vs bare soil 11.6% is a relative increase of (15.1 - 11.6) / 11.6 = 30.2% (3.5 percentage points)",
|
|
55
|
+
"Does not invent references, author details, figures, data or results not in the file"
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"id": 5,
|
|
60
|
+
"held_out": "2026-11-09",
|
|
61
|
+
"prompt": "This is chapter 3 of my PhD thesis (chapter3.md). My supervisor, Dr. Imelda Faraday-Nwosu, wants the full draft by Friday and she is strict about references. Department style is numbered references in order of first citation in the text. Could you check that every citation matches the reference list, that the references are numbered correctly, and that the figure, table and section cross-references all work? She also reads closely for whether the abstract and results hang together with the methods, so flag anything there too. Please tell me exactly what to change, including any renumbering.",
|
|
62
|
+
"assertions": [
|
|
63
|
+
"Flags that references are not numbered in order of first citation: the text cites [1], [2], [5], [3], [4] in that order, so the fix renumbers old 5 to 3, old 3 to 4 and old 4 to 5 (1, 2, 6, 7 and 8 keep their numbers), updating both the list and the in-text citations",
|
|
64
|
+
"Flags references 7 and 9 (Hargreaves & Nakamura 2024, Battery variability in light electric vehicle fleets) as the same work listed twice in different journal-name styles; the fix merges them so the [9] citation in Section 3.2.3 becomes [7], leaving an 8-entry list",
|
|
65
|
+
"Flags citation [11] in the Results as having no entry in the reference list (the list ends at 9) and asks for the source rather than inventing one; once supplied it becomes the next number in order of first citation (9 after the merge)",
|
|
66
|
+
"Flags the cross-reference to 'Section 3.2.4' in the Introduction: Methods has only 3.2.1-3.2.3, and the likelier target is 3.2.2 (Charging regimes); and flags that Figure 2 (monthly depot temperature) has a caption but is never referred to in the text",
|
|
67
|
+
"Flags that the abstract's fleet-wide mean fade of 7.9% disagrees with Table 1's 'All bikes' figure of 7.0% (consistent with (27 x 6.4 + 15 x 8.1) / 42 = 7.0%), so the abstract is likely a transposition and should read 7.0%",
|
|
68
|
+
"Flags that the Results report a Kaplan-Meier analysis of time to 80% capacity that the Methods (3.2.3) never describe, so it must be added to the methods or dropped",
|
|
69
|
+
"Does not invent references, bibliographic details, data, figures or analyses not in the file"
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Peer mentoring and first-year retention: a mixed-methods study
|
|
2
|
+
|
|
3
|
+
## Abstract
|
|
4
|
+
|
|
5
|
+
We studied whether pairing first-year students with trained second-year mentors improves retention into the second year. Across three cohorts (n = 1,240), mentored students were retained at 91% against 84% for a matched comparison group. Interviews suggest the effect works through belonging rather than academic help.
|
|
6
|
+
|
|
7
|
+
## 1. Introduction
|
|
8
|
+
|
|
9
|
+
Retention in the first year predicts degree completion (Tinto, 1993; Kuh et al., 2008). Peer mentoring is widely used but rarely evaluated with a comparison group (Crisp & Cruz, 2009). Walton and Cohen (2011) showed that a brief belonging intervention raised grades, and Yeager (2016) extended the finding to larger samples.
|
|
10
|
+
|
|
11
|
+
Section 4 sets out the analysis, and Section 6 discusses limits.
|
|
12
|
+
|
|
13
|
+
## 2. Method
|
|
14
|
+
|
|
15
|
+
Students were matched on entry tariff and background (Rosenbaum & Rubin, 1983). Figure 1 shows the matching, and Table 1 summarises the cohorts.
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
**Figure 1.** Propensity-score matching of mentored and comparison students.
|
|
19
|
+
|
|
20
|
+
**Table 1.** Cohort sizes and retention by group.
|
|
21
|
+
|
|
22
|
+
| Cohort | Mentored | Comparison |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| 2022 | 410 | 398 |
|
|
25
|
+
|
|
26
|
+
## 3. Results
|
|
27
|
+
|
|
28
|
+
Mentored students were more likely to be retained (Table 2). Figure 3 shows the effect by cohort. The effect was strongest for first-generation students (Stephens et al., 2012).
|
|
29
|
+
|
|
30
|
+
**Figure 2.** Retention by cohort and group.
|
|
31
|
+
|
|
32
|
+
## 4. Discussion
|
|
33
|
+
|
|
34
|
+
The pattern supports a belonging account (Walton & Cohen, 2011; Strayhorn, 2019) over an academic-support account (Kuh et al., 2009).
|
|
35
|
+
|
|
36
|
+
## References
|
|
37
|
+
|
|
38
|
+
Crisp, G., & Cruz, I. (2009). Mentoring college students: A critical review of the literature between 1990 and 2007. *Research in Higher Education*, 50(6), 525–545. https://doi.org/10.1007/s11162-009-9130-2
|
|
39
|
+
|
|
40
|
+
Kuh, G. D., Cruce, T. M., Shoup, R., Kinzie, J., & Gonyea, R. M. (2008). Unmasking the effects of student engagement on first-year college grades and persistence. *The Journal of Higher Education*, 79(5), 540–563.
|
|
41
|
+
|
|
42
|
+
Rosenbaum, P. R., & Rubin, D. B. (1983). The central role of the propensity score in observational studies for causal effects. *Biometrika*, 70(1), 41–55. https://doi.org/10.1093/biomet/70.1.41
|
|
43
|
+
|
|
44
|
+
Stephens, N. M., Fryberg, S. A., Markus, H. R., Johnson, C. S., & Covarrubias, R. (2012). Unseen disadvantage: How American universities' focus on independence undermines the academic performance of first-generation college students. *Journal of Personality and Social Psychology*, 102(6), 1178–1197.
|
|
45
|
+
|
|
46
|
+
Strayhorn, T. L. (2019). *College students' sense of belonging* (2nd ed.). Routledge.
|
|
47
|
+
|
|
48
|
+
Tinto, V. (1993). *Leaving college: Rethinking the causes and cures of student attrition* (2nd ed.). University of Chicago Press.
|
|
49
|
+
|
|
50
|
+
Walton, G. M., & Cohen, G. L. (2011). A brief social-belonging intervention improves academic and health outcomes of minority students. *Science*, 331(6023), 1447–1451.
|
|
51
|
+
|
|
52
|
+
Yeagar, D. S. (2016). Teaching a lay theory before college narrows achievement gaps at scale. *PNAS*, 113(24), E3341–E3348.
|
|
53
|
+
|
|
54
|
+
Pascarella, E. T., & Terenzini, P. T. (2005). *How college affects students* (Vol. 2). Jossey-Bass.
|
|
55
|
+
|
|
56
|
+
Rosenbaum, P. R., & Rubin, D. B. (1983). The central role of the propensity score in observational studies for causal effects. *Biometrika*, 70(1), 41–55. doi:10.1093/biomet/70.1.41
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Rooftop Forb Plantings Increase Solitary Bee Nesting in a Mid-Sized City
|
|
2
|
+
|
|
3
|
+
Running head: ROOFTOP FORBS AND SOLITARY BEES
|
|
4
|
+
|
|
5
|
+
## Abstract
|
|
6
|
+
|
|
7
|
+
Green roofs are often promoted as pollinator habitat, but most are planted with
|
|
8
|
+
sedum mats that offer little forage. We compared solitary bee nesting on 18 roofs in
|
|
9
|
+
Carrowmere: six sedum-only roofs, six mixed forb roofs and six bare control roofs.
|
|
10
|
+
Trap nests on forb roofs held three times as many brood cells as those on sedum roofs,
|
|
11
|
+
and nest occupancy rose with forb cover. We argue that planting choice, not roof
|
|
12
|
+
area, is the main lever for city planners.
|
|
13
|
+
|
|
14
|
+
## Introduction
|
|
15
|
+
|
|
16
|
+
Cities can support a surprising diversity of wild bees when floral resources and
|
|
17
|
+
nesting sites are both available (Quillfeather & Arslan, 2015). Cavity-nesting
|
|
18
|
+
species in particular readily colonise artificial trap nests placed in urban
|
|
19
|
+
settings (Achterberg & Nwosu, 2019), which makes them a practical study group for
|
|
20
|
+
comparing habitat treatments.
|
|
21
|
+
|
|
22
|
+
Green roofs have been proposed as stepping stones that connect fragmented urban
|
|
23
|
+
habitat (Haldane-Ruiz et al., 2016). Most installed roofs, however, are extensive
|
|
24
|
+
systems planted with drought-tolerant sedum, chosen for low maintenance rather than
|
|
25
|
+
ecological value (Okonkwo-Varga, 2019). Sedum flowers briefly and attracts mainly
|
|
26
|
+
generalist visitors (Lindqvst & Moreau, 2020).
|
|
27
|
+
|
|
28
|
+
Forb-rich roofs can extend the flowering season, but they need deeper substrate and
|
|
29
|
+
more care, and city planners have little evidence that the extra cost pays off for
|
|
30
|
+
pollinators (Tamberlane, 2021). Earlier roof surveys counted foraging bees rather than
|
|
31
|
+
nests, so it remains unclear whether roofs support reproduction or only feeding
|
|
32
|
+
(Pemberton-Shah, 2022).
|
|
33
|
+
|
|
34
|
+
Here we ask whether forb plantings increase solitary bee nesting on roofs compared
|
|
35
|
+
with sedum plantings and bare control roofs.
|
|
36
|
+
|
|
37
|
+
## Method
|
|
38
|
+
|
|
39
|
+
### Study sites
|
|
40
|
+
|
|
41
|
+
We selected 18 flat roofs in central Carrowmere, all between four and six storeys
|
|
42
|
+
high and at least 300 m apart (Figure 1). Six were sedum-only extensive roofs, six
|
|
43
|
+
had been converted to mixed native forb plantings at least three years before the
|
|
44
|
+
study, and six were bare gravel roofs used as controls.
|
|
45
|
+
|
|
46
|
+
### Trap nests
|
|
47
|
+
|
|
48
|
+
On each roof we placed four trap nests, each holding 40 reed and cardboard tubes of
|
|
49
|
+
three diameters, following the design of Vasquez-Tolland (2020). Nests were set out
|
|
50
|
+
in late March and collected in October.
|
|
51
|
+
|
|
52
|
+
### Floral surveys
|
|
53
|
+
|
|
54
|
+
Each month from April to September we recorded percentage forb cover in ten 1 m2
|
|
55
|
+
quadrats per roof, using the protocol of Delacroix-Hume and Petrov (2021).
|
|
56
|
+
|
|
57
|
+
### Analysis
|
|
58
|
+
|
|
59
|
+
We modelled brood cells per nest with a negative binomial mixed model, with roof type
|
|
60
|
+
and forb cover as fixed effects and roof as a random effect.
|
|
61
|
+
|
|
62
|
+
## Results
|
|
63
|
+
|
|
64
|
+
Across all roofs we recorded 2,184 brood cells from nine species. Mean brood cells per
|
|
65
|
+
nest were 31.2 on forb roofs, 10.4 on sedum roofs and 2.1 on control roofs (Table 1).
|
|
66
|
+
|
|
67
|
+
Brood cell counts rose with forb cover on both planted roof types (Figure 2). The
|
|
68
|
+
relationship held after removing the two roofs closest to the city park.
|
|
69
|
+
|
|
70
|
+
Mason bees made up most of the cells on forb roofs, while leafcutter bees were
|
|
71
|
+
recorded only on forb roofs with more than 30% cover (Figure 3).
|
|
72
|
+
|
|
73
|
+
Table 1
|
|
74
|
+
Mean brood cells per trap nest by roof type
|
|
75
|
+
|
|
76
|
+
| Roof type | Roofs | Mean brood cells per nest | SD |
|
|
77
|
+
|---|---|---|---|
|
|
78
|
+
| Mixed forb | 6 | 31.2 | 8.7 |
|
|
79
|
+
| Sedum | 6 | 10.4 | 4.1 |
|
|
80
|
+
| Control | 6 | 2.1 | 1.6 |
|
|
81
|
+
|
|
82
|
+
Figure 1. Locations of the 18 study roofs in central Carrowmere.
|
|
83
|
+
|
|
84
|
+
Figure 2. Brood cells per nest against mean forb cover, by roof type.
|
|
85
|
+
|
|
86
|
+
## Discussion
|
|
87
|
+
|
|
88
|
+
Our results suggest that roofs can support solitary bee reproduction, not just
|
|
89
|
+
foraging, provided they are planted with forbs. This agrees with ground-level work
|
|
90
|
+
showing that nesting responds to floral cover within a few hundred metres
|
|
91
|
+
(Quillfeather & Arslan, 2015; Achterberg & Nwosu, 2019).
|
|
92
|
+
|
|
93
|
+
The low counts on sedum roofs match the view that sedum offers a short, narrow
|
|
94
|
+
flowering window (Lindqvst & Moreau, 2020). Planners weighing the higher cost of forb
|
|
95
|
+
roofs should set it against this difference in nesting (Pemberton-Shah, 2022).
|
|
96
|
+
|
|
97
|
+
Our study covered one season in one city. Longer studies should track whether
|
|
98
|
+
nesting on forb roofs persists as plantings age (Haldane-Ruiz et al., 2016).
|
|
99
|
+
|
|
100
|
+
## References
|
|
101
|
+
|
|
102
|
+
Achterberg, L., & Nwosu, P. (2019). Trap-nest colonisation by cavity-nesting bees along an urban gradient. *Journal of Rooftop Ecology*, 7(2), 44-58.
|
|
103
|
+
|
|
104
|
+
Delacroix-Hume, Brigitte, & Petrov, Stefan (2021). A quadrat protocol for flowering cover on extensive roofs. *Pollinator Habitat Review*, 12(1), 3-17.
|
|
105
|
+
|
|
106
|
+
Feroz, A. (2017). Substrate depth and forb survival on extensive green roofs. *Built Environment Biology*, 3(1), 12-29.
|
|
107
|
+
|
|
108
|
+
Haldane-Ruiz, M., Ostrowski, K., & Bell, T. (2016). Green roofs as stepping stones for urban insects. *Journal of Rooftop Ecology*, 4(3), 101-119.
|
|
109
|
+
|
|
110
|
+
Lindqvist, E., & Moreau, C. (2020). Flowering phenology and visitor networks on sedum roofs. *Pollinator Habitat Review*, 11(4), 210-226.
|
|
111
|
+
|
|
112
|
+
Okonkwo-Varga, J. (2018). Why cities plant sedum: Maintenance, cost and the green roof market. *Built Environment Biology*, 4(2), 55-71.
|
|
113
|
+
|
|
114
|
+
Pemberton-Shah, R. (2022). Foraging bees on city roofs: A five-city survey. *Carrowmere Naturalist*, 88(1), 9-24.
|
|
115
|
+
|
|
116
|
+
Quillfeather, N., & Arslan, D. (2015). Floral resources and nest sites limit wild bees in cities. *Journal of Rooftop Ecology*, 3(1), 1-15.
|
|
117
|
+
|
|
118
|
+
Vasquez-Tolland, H. (2020). A low-cost trap nest for monitoring solitary bees. *Carrowmere Naturalist*, 86(2), 77-85.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Pilot Evaluation of a Low-Cost Soil Moisture Sensor Network at Fernhollow Community Farm
|
|
2
|
+
|
|
3
|
+
Report to the Fernhollow Farm Trust board, October 2026
|
|
4
|
+
Author: Tomasz Arkwright-Bello, Fernhollow Community Farm (fernhollow.example)
|
|
5
|
+
|
|
6
|
+
## Abstract
|
|
7
|
+
|
|
8
|
+
Fernhollow Community Farm installed 24 low-cost capacitive soil moisture sensors
|
|
9
|
+
across its market garden in spring 2026 to see whether sensor readings could replace
|
|
10
|
+
the hand-held probe rounds that volunteers carry out twice a week. Over 20 weeks the
|
|
11
|
+
network delivered 91% of expected readings. After site calibration, sensor readings
|
|
12
|
+
agreed with the reference probe to within 3.1 percentage points of volumetric water
|
|
13
|
+
content. Irrigation water use fell by 18% compared with the same period in 2025. We
|
|
14
|
+
recommend extending the network to the orchard in 2027, subject to the battery and
|
|
15
|
+
enclosure changes set out below.
|
|
16
|
+
|
|
17
|
+
## 1. Introduction
|
|
18
|
+
|
|
19
|
+
Small farms rarely have the budget for research-grade soil sensors, which can cost
|
|
20
|
+
several hundred pounds per probe [1]. Low-cost capacitive sensors have become widely
|
|
21
|
+
available, but their readings drift with temperature and soil type [2], and few
|
|
22
|
+
studies have tested them on mixed vegetable plots run by volunteers.
|
|
23
|
+
|
|
24
|
+
Calibrating each sensor against a reference instrument in the soil where it will be
|
|
25
|
+
used improves accuracy considerably [4]. Wireless networks also bring practical
|
|
26
|
+
problems: radio range across polytunnels, battery life and damage from tools and
|
|
27
|
+
animals [3]. Community growers have reported that data dashboards are only used if
|
|
28
|
+
they answer a simple question, such as which beds need water today [5].
|
|
29
|
+
|
|
30
|
+
This report describes a 20-week pilot at Fernhollow and asks three questions. Did the
|
|
31
|
+
network deliver data reliably? After calibration, how close were sensor readings to
|
|
32
|
+
the reference probe? Did watering change as a result?
|
|
33
|
+
|
|
34
|
+
## 2. Methods
|
|
35
|
+
|
|
36
|
+
### 2.1 Site and sensor layout
|
|
37
|
+
|
|
38
|
+
The market garden covers 0.6 ha and is divided into 12 beds and two polytunnels. Two
|
|
39
|
+
sensors were installed in each bed at 10 cm and 25 cm depth, giving 24 sensors in total
|
|
40
|
+
(Figure 1). Each sensor was connected to a solar-powered radio node based on an open
|
|
41
|
+
hardware design [6].
|
|
42
|
+
|
|
43
|
+
### 2.2 Calibration and reference measurements
|
|
44
|
+
|
|
45
|
+
Each sensor was calibrated in the field against a hand-held reference probe over five
|
|
46
|
+
visits in April, following the two-point method of Delahaye and Strand [7]. Volunteers
|
|
47
|
+
then took reference readings beside every sensor twice a week for the rest of the
|
|
48
|
+
season. Readings were sent every 15 minutes to a gateway in the farm office and logged
|
|
49
|
+
to a shared spreadsheet [8].
|
|
50
|
+
|
|
51
|
+
## 3. Results
|
|
52
|
+
|
|
53
|
+
### 3.1 Data delivery
|
|
54
|
+
|
|
55
|
+
Of 322,560 expected readings, 293,530 (91%) were received. Most losses came from two
|
|
56
|
+
nodes in the far polytunnel, where the radio link failed during wet weather; these are
|
|
57
|
+
discussed in Section 4.3.
|
|
58
|
+
|
|
59
|
+
### 3.2 Agreement with the reference probe
|
|
60
|
+
|
|
61
|
+
Before calibration, the mean absolute difference between sensors and the reference
|
|
62
|
+
probe was 7.8 percentage points. After calibration it fell to 3.1 points, comparable
|
|
63
|
+
with results reported for similar sensors in loam soils [10]. Agreement was poorer at
|
|
64
|
+
25 cm than at 10 cm in the clay-rich beds (Table 1).
|
|
65
|
+
|
|
66
|
+
Table 1. Mean absolute difference from the reference probe (percentage points of
|
|
67
|
+
volumetric water content), after calibration.
|
|
68
|
+
|
|
69
|
+
| Bed soil | 10 cm | 25 cm |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| Sandy loam (beds 1-6) | 2.4 | 2.9 |
|
|
72
|
+
| Clay loam (beds 7-12) | 3.0 | 4.6 |
|
|
73
|
+
| Polytunnels | 2.7 | 3.3 |
|
|
74
|
+
|
|
75
|
+
Figure 1. Plan of the market garden showing the 24 sensor positions and the gateway.
|
|
76
|
+
|
|
77
|
+
Figure 2. Daily mean soil moisture at 10 cm in bed 4, May to September 2026, with
|
|
78
|
+
irrigation events marked.
|
|
79
|
+
|
|
80
|
+
## 4. Discussion
|
|
81
|
+
|
|
82
|
+
### 4.1 Reliability and accuracy
|
|
83
|
+
|
|
84
|
+
The network was reliable enough to support day-to-day watering decisions, and the
|
|
85
|
+
accuracy after calibration was in line with other low-cost deployments [10]. The
|
|
86
|
+
importance of calibrating in the actual soil, rather than using factory settings, is
|
|
87
|
+
consistent with earlier community trials [11].
|
|
88
|
+
|
|
89
|
+
### 4.2 Effect on watering
|
|
90
|
+
|
|
91
|
+
Irrigation water use fell by 18% compared with the same weeks in 2025. Volunteers said
|
|
92
|
+
the dashboard made it easier to skip beds that were still moist, which supports the
|
|
93
|
+
point that simple displays drive use [5]. Water savings of a similar size have been
|
|
94
|
+
reported on allotment sites [12].
|
|
95
|
+
|
|
96
|
+
## 5. Conclusion
|
|
97
|
+
|
|
98
|
+
A low-cost sensor network can give a small community farm usable soil moisture data,
|
|
99
|
+
provided each sensor is calibrated in place. We recommend extending it to the orchard
|
|
100
|
+
in 2027 with sealed enclosures and larger batteries.
|
|
101
|
+
|
|
102
|
+
## References
|
|
103
|
+
|
|
104
|
+
1. Abernathy-Coles R, Prasad V. The cost of soil sensing on small farms. J Field Instrum Stud. 2019;14(2):101-9.
|
|
105
|
+
2. Mirembe K, Laskaris T. Temperature drift in capacitive soil moisture sensors. J Field Instrum Stud. 2020;15(1):22-31.
|
|
106
|
+
3. Ferreira-Lund A, Odhiambo S, Kask P. Radio range and battery life in farm sensor networks. Small Farm Technol. 2021;6(3):140-52.
|
|
107
|
+
4. Yarborough D. Site-specific calibration of low-cost soil probes. Soil Meas Lett. 2018;9(4):77-84.
|
|
108
|
+
5. Castellanos M, Whitcombe J. What community growers want from farm data dashboards. Community Hortic Rev. 2022;3(1):5-18.
|
|
109
|
+
6. Brannigan L. An open hardware node for agricultural sensing. Open Agric Hardw. 2021;2(2):33-41.
|
|
110
|
+
7. Delahaye, P., & Strand, O. (2017). A two-point field calibration for capacitive soil sensors. Soil Measurement Letters, 8(2), 50-58.
|
|
111
|
+
8. Nakashima R, Pell G. Logging farm sensor data to shared spreadsheets. Small Farm Technol. 2020;5(1):12-19.
|
|
112
|
+
9. Okafor-Brandt E. Soil moisture monitoring in urban orchards. Community Hortic Rev. 2023;4(2):61-70.
|
|
113
|
+
10. Iversen H, Achebe-Lowe C. Accuracy of low-cost moisture sensors in loam soils. Soil Meas Lett. 2022;13(1):1-12.
|
|
114
|
+
11. Castellanos M, Whitcombe J. What community growers want from farm data dashboards. Community Hortic Rev. 2022;3(1):5-18.
|
|
115
|
+
12. Rutherford-Diaz S. Water savings from sensor-guided irrigation on allotments. Small Farm Technol. 2023;8(2):88-97.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Mulch type and summer soil moisture in Brackenford community gardens
|
|
2
|
+
|
|
3
|
+
Rosalind Achebe-Mortlake and Tobias Wren
|
|
4
|
+
Department of Urban Ecology, Brackenford Polytechnic (brackenford-poly.example)
|
|
5
|
+
|
|
6
|
+
## Abstract
|
|
7
|
+
|
|
8
|
+
Community gardeners in temperate cities increasingly face dry summers, and mulching is the most common water-saving advice they receive. We compared four surface treatments (bare soil, straw, wood chip and green-waste compost) in 48 raised beds across six community gardens in Brackenford over the summer of 2025. Volumetric soil moisture was logged hourly at 10 cm depth from June to August. Wood-chip mulch raised mean soil moisture by 23% relative to bare soil, and all three mulches reduced the number of days on which moisture fell below the wilting threshold. We recommend wood chip as the default mulch for allotment-style beds where it is locally available.
|
|
9
|
+
|
|
10
|
+
## 1. Introduction
|
|
11
|
+
|
|
12
|
+
Urban food growing has expanded rapidly over the last decade (Abernathy & Solberg, 2015; Calloway, 2018), and community gardens now supply a meaningful share of fresh vegetables for some low-income households (Dunmore et al., 2020). These gardens are often on compacted, low-organic-matter soils that dry quickly (Halvorsen, 2017).
|
|
13
|
+
|
|
14
|
+
Mulches reduce evaporation from the soil surface and moderate soil temperature (Quaresma, 2014). Straw and wood chip are widely recommended in gardening guidance, but field comparisons in small urban beds are scarce. Pemberton (2017) found that compost mulch improved moisture retention in rural market gardens, while Marchetti and Oyelaran (2021) reported that coarse wood chip outperformed finer materials in park plantings.
|
|
15
|
+
|
|
16
|
+
The aim of this study was to compare the effect of common, low-cost mulches on summer soil moisture in working community gardens.
|
|
17
|
+
|
|
18
|
+
## 2. Methods
|
|
19
|
+
|
|
20
|
+
### 2.1 Sites and design
|
|
21
|
+
|
|
22
|
+
Six community gardens across Brackenford were recruited through the city's allotment federation. In each garden, eight raised beds of similar size (1.2 m x 2.4 m) and soil were assigned at random to one of four treatments, giving two beds per treatment per garden and 48 beds in total (Figure 1).
|
|
23
|
+
|
|
24
|
+
### 2.2 Treatments
|
|
25
|
+
|
|
26
|
+
Mulches were applied in late May 2025 at a depth of 5 cm. Straw was locally sourced barley straw; wood chip was municipal arboricultural chip; compost was the city's certified green-waste compost. Bare beds were hand-weeded weekly.
|
|
27
|
+
|
|
28
|
+
### 2.3 Measurements
|
|
29
|
+
|
|
30
|
+
A capacitance soil-moisture sensor was installed at 10 cm depth in the centre of each bed and logged hourly from 1 June to 31 August 2025. Days below the wilting threshold were counted using the threshold for sandy loam given by Weatherby and Zhou (2022).
|
|
31
|
+
|
|
32
|
+
### 2.4 Analysis
|
|
33
|
+
|
|
34
|
+
Daily mean moisture per bed was analysed with a linear mixed-effects model with treatment as a fixed effect and garden as a random effect.
|
|
35
|
+
|
|
36
|
+
## 3. Results
|
|
37
|
+
|
|
38
|
+
Mean volumetric soil moisture differed between treatments (Table 1). All three mulches kept beds wetter than bare soil, with wood chip the wettest throughout the season (Figure 2).
|
|
39
|
+
|
|
40
|
+
Table 1. Mean volumetric soil moisture and days below wilting threshold, June to August 2025 (n = 12 beds per treatment).
|
|
41
|
+
|
|
42
|
+
| Treatment | Mean soil moisture (%) | Days below wilting threshold |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| Bare soil | 11.6 | 31 |
|
|
45
|
+
| Straw | 14.2 | 17 |
|
|
46
|
+
| Wood chip | 15.1 | 12 |
|
|
47
|
+
| Compost | 13.0 | 22 |
|
|
48
|
+
|
|
49
|
+
The difference between wood chip and bare soil was largest during the July heatwave, when bare beds fell below the wilting threshold on 19 consecutive days (Figure 3). Compost beds dried faster than the other mulched beds after rain.
|
|
50
|
+
|
|
51
|
+
Figure 1. Layout of the 48 raised beds across the six gardens.
|
|
52
|
+
|
|
53
|
+
Figure 2. Daily mean soil moisture by treatment, 1 June to 31 August 2025.
|
|
54
|
+
|
|
55
|
+
## 4. Discussion
|
|
56
|
+
|
|
57
|
+
Our results agree with the field trials of Marchetti and Oyelaran (2021): coarse wood chip retained the most moisture. The weaker performance of compost may reflect its fine texture and the way it is incorporated into the soil surface by worms within weeks of application (Varga, 2021). Straw performed well but blew off two beds during a July storm, which gardeners reported as a practical drawback.
|
|
58
|
+
|
|
59
|
+
Wood chip is available free from the city's tree contractors, which makes it the most practical choice for most Brackenford gardens. Further work should test whether these effects persist over several seasons as mulches decompose.
|
|
60
|
+
|
|
61
|
+
## References
|
|
62
|
+
|
|
63
|
+
Abernathy, K., & Solberg, P. (2015). Growing food in the post-industrial city. *Journal of Urban Horticulture Example*, 12(3), 201-215.
|
|
64
|
+
|
|
65
|
+
Calloway, R. (2018). *Plots and people: A history of the allotment movement*. Fenwick Academic Press.
|
|
66
|
+
|
|
67
|
+
Dunmore, S., Ferreira, L., & Ishii, T. (2020). Community gardens and household vegetable supply in three northern cities. *Urban Food Systems Example*, 7(1), 44-59.
|
|
68
|
+
|
|
69
|
+
Halvorsen, M. (2019). Soil compaction and organic matter in urban growing spaces. *Soil Use and Cities Example*, 35(2), 118-130.
|
|
70
|
+
|
|
71
|
+
Lindqvist, E. (2016). Irrigation habits of hobby gardeners. *Garden Science Example*, 4(4), 300-312.
|
|
72
|
+
|
|
73
|
+
Marchetti, G., & Oyelaran, B. (2021). Particle size of organic mulches and soil water in park plantings. *Arboricultural Practice Example*, 47(1), 23-37.
|
|
74
|
+
|
|
75
|
+
Pembleton, A. (2017). Compost mulch and moisture retention in market gardens. *Small Farm Research Example*, 9(2), 77-90.
|
|
76
|
+
|
|
77
|
+
Quaresma, D. (2014). Surface mulches, evaporation and soil temperature: A review. *Agronomy Reviews Example*, 21, 1-26.
|
|
78
|
+
|
|
79
|
+
Weatherby, J., & Zhou, H. (2022). Wilting thresholds for common garden soils. *Soil Use and Cities Example*, 38(4), 410-422.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Chapter 3: Capacity fade in a shared electric cargo-bike fleet
|
|
2
|
+
|
|
3
|
+
## Abstract
|
|
4
|
+
|
|
5
|
+
Shared electric cargo bikes are spreading through European city centres, but operators have little evidence on how quickly their batteries wear out in daily commercial use. This chapter follows the 42 bikes of the Wrenhaven municipal cargo-bike scheme for 18 months, measuring usable battery capacity every month. Fleet-wide mean capacity fade over the study period was 7.9%. Bikes charged on the depot's fast chargers lost capacity faster than those on standard chargers. Capacity fade was driven mainly by cumulative charge cycles rather than calendar age. These results give operators a basis for planning battery replacement.
|
|
6
|
+
|
|
7
|
+
## 3.1 Introduction
|
|
8
|
+
|
|
9
|
+
Cargo bikes are replacing vans for many last-mile deliveries in dense urban areas [1]. Electric assistance has made them practical for heavier loads and longer routes [2], and several cities now run shared fleets for small businesses and residents [5]. Battery replacement is the largest single maintenance cost for these fleets [3], yet most published data on lithium-ion degradation comes from laboratory cycling or from private electric cars [4].
|
|
10
|
+
|
|
11
|
+
Laboratory studies show that high charging currents and high temperatures accelerate capacity fade [6]. Whether this matters in a cargo-bike fleet, where batteries are small and charging is often supervised, is not known. Fleet operators also report wide variation between nominally identical batteries [7], which makes replacement planning difficult.
|
|
12
|
+
|
|
13
|
+
This chapter asks two questions: how fast do batteries in a working shared fleet lose capacity, and does the charging regime make a difference? The fleet data and the operator interviews that motivated the study are introduced in Chapter 2; the depot charging schedule is described in Section 3.2.4.
|
|
14
|
+
|
|
15
|
+
## 3.2 Methods
|
|
16
|
+
|
|
17
|
+
### 3.2.1 Fleet and data collection
|
|
18
|
+
|
|
19
|
+
The Wrenhaven scheme operates 42 identical cargo bikes (48 V, 750 Wh lithium-ion packs) from a single depot. All bikes entered service in March 2024 with new batteries. Between April 2024 and September 2025, each battery was removed once a month and its usable capacity measured with a constant-current discharge test at 20 degrees C, following the protocol of Sandoval and Kettunen [8].
|
|
20
|
+
|
|
21
|
+
### 3.2.2 Charging regimes
|
|
22
|
+
|
|
23
|
+
For operational reasons the depot assigns bikes to one of two charging bays. Twenty-seven bikes were charged on standard 2 A chargers and fifteen on 5 A fast chargers. Assignment was fixed for the whole study period and followed the bays' original wiring rather than any property of the bikes.
|
|
24
|
+
|
|
25
|
+
### 3.2.3 Statistical analysis
|
|
26
|
+
|
|
27
|
+
Capacity fade was expressed as the percentage loss from each battery's first measured capacity. A linear mixed-effects model was fitted with capacity fade as the outcome, cumulative charge cycles, months in service and charging regime as fixed effects, and bike as a random effect [9]. Models were fitted in R.
|
|
28
|
+
|
|
29
|
+
## 3.3 Results
|
|
30
|
+
|
|
31
|
+
All 42 batteries completed the 18 months of testing. Mean capacity fade at the final test is shown by charging regime in Table 1.
|
|
32
|
+
|
|
33
|
+
Table 1. Capacity fade after 18 months by charging regime.
|
|
34
|
+
|
|
35
|
+
| Charging regime | Bikes (n) | Mean capacity fade (%) | Mean charge cycles |
|
|
36
|
+
|---|---|---|---|
|
|
37
|
+
| Standard (2 A) | 27 | 6.4 | 410 |
|
|
38
|
+
| Fast (5 A) | 15 | 8.1 | 455 |
|
|
39
|
+
| All bikes | 42 | 7.0 | 426 |
|
|
40
|
+
|
|
41
|
+
In the mixed-effects model, cumulative charge cycles were strongly associated with capacity fade, while months in service added little once cycles were accounted for (Figure 1). Fast-charged bikes lost on average 1.7 percentage points more capacity than standard-charged bikes, a difference consistent with laboratory findings [6, 11].
|
|
42
|
+
|
|
43
|
+
A Kaplan-Meier analysis of time to 80% of initial capacity found that no battery reached the threshold during the study; on current trends, median time to 80% is projected at roughly 4.5 years for standard-charged bikes.
|
|
44
|
+
|
|
45
|
+
Figure 1. Capacity fade against cumulative charge cycles for all 42 bikes, coloured by charging regime.
|
|
46
|
+
|
|
47
|
+
Figure 2. Monthly mean depot temperature, April 2024 to September 2025.
|
|
48
|
+
|
|
49
|
+
## 3.4 Discussion
|
|
50
|
+
|
|
51
|
+
The fleet's batteries are wearing more slowly than operators feared. Cycle count, not age, is the main driver, which suggests operators should rotate bikes between heavy and light routes to even out wear across the fleet [7]. The fast-charging penalty is modest but real, and the depot could reserve fast chargers for bikes needed back on the road quickly.
|
|
52
|
+
|
|
53
|
+
The main limitation is that charging regime was not randomly assigned. The bays differ in position within the depot, and bikes on the fast-charging bay may also have been used on longer routes. Future work should randomise bikes between bays.
|
|
54
|
+
|
|
55
|
+
## References
|
|
56
|
+
|
|
57
|
+
1. Ashworth P, Delacroix M. Cargo bikes and the last mile: a review. Urban Logistics Example. 2021;9(2):55-71.
|
|
58
|
+
2. Bramley T, Okafor N, Liu S. Electric assistance and load capacity in utility cycling. Journal of Active Transport Example. 2022;14(1):12-25.
|
|
59
|
+
3. Castellanos R. Maintenance costs of shared micromobility fleets. Transport Operations Example. 2023;6(3):201-214.
|
|
60
|
+
4. Dahlgren E, Mbeki T. Real-world battery degradation in private electric vehicles. Energy and Mobility Example. 2020;11(4):340-356.
|
|
61
|
+
5. Elsworth J. Shared cargo-bike schemes in European cities: an inventory. Cities in Motion Example. 2023;3(1):1-18.
|
|
62
|
+
6. Fontaine L, Grigoriev A. Charging current, temperature and capacity fade in lithium-ion cells. Battery Science Example. 2019;27(5):600-615.
|
|
63
|
+
7. Hargreaves D, Nakamura Y. Battery variability in light electric vehicle fleets. Journal of Fleet Management Example. 2024;2(2):88-97.
|
|
64
|
+
8. Sandoval C, Kettunen O. A field protocol for capacity testing of e-bike batteries. Cycling Engineering Example. 2022;5(1):30-41.
|
|
65
|
+
9. Hargreaves D, Nakamura Y. Battery variability in light electric vehicle fleets. J Fleet Manag Example. 2024;2(2):88-97.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Peer mentoring and first-year retention: a mixed-methods study
|
|
2
|
+
|
|
3
|
+
## Abstract
|
|
4
|
+
|
|
5
|
+
We studied whether pairing first-year students with trained second-year mentors improves retention into the second year. Across three cohorts (n = 1,240), mentored students were retained at 91% against 84% for a matched comparison group. Interviews suggest the effect works through belonging rather than academic help.
|
|
6
|
+
|
|
7
|
+
## 1. Introduction
|
|
8
|
+
|
|
9
|
+
Retention in the first year predicts degree completion (Tinto, 1993; Kuh et al., 2008). Peer mentoring is widely used but rarely evaluated with a comparison group (Crisp & Cruz, 2009). Walton and Cohen (2011) showed that a brief belonging intervention raised grades, and Yeager (2016) extended the finding to larger samples.
|
|
10
|
+
|
|
11
|
+
Section 4 sets out the analysis, and Section 6 discusses limits.
|
|
12
|
+
|
|
13
|
+
## 2. Method
|
|
14
|
+
|
|
15
|
+
Students were matched on entry tariff and background (Rosenbaum & Rubin, 1983). Figure 1 shows the matching, and Table 1 summarises the cohorts.
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
**Figure 1.** Propensity-score matching of mentored and comparison students.
|
|
19
|
+
|
|
20
|
+
**Table 1.** Cohort sizes and retention by group.
|
|
21
|
+
|
|
22
|
+
| Cohort | Mentored | Comparison |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| 2022 | 410 | 398 |
|
|
25
|
+
|
|
26
|
+
## 3. Results
|
|
27
|
+
|
|
28
|
+
Mentored students were more likely to be retained (Table 2). Figure 3 shows the effect by cohort. The effect was strongest for first-generation students (Stephens et al., 2012).
|
|
29
|
+
|
|
30
|
+
**Figure 2.** Retention by cohort and group.
|
|
31
|
+
|
|
32
|
+
## 4. Discussion
|
|
33
|
+
|
|
34
|
+
The pattern supports a belonging account (Walton & Cohen, 2011; Strayhorn, 2019) over an academic-support account (Kuh et al., 2009).
|
|
35
|
+
|
|
36
|
+
## References
|
|
37
|
+
|
|
38
|
+
Crisp, G., & Cruz, I. (2009). Mentoring college students: A critical review of the literature between 1990 and 2007. *Research in Higher Education*, 50(6), 525–545. https://doi.org/10.1007/s11162-009-9130-2
|
|
39
|
+
|
|
40
|
+
Kuh, G. D., Cruce, T. M., Shoup, R., Kinzie, J., & Gonyea, R. M. (2008). Unmasking the effects of student engagement on first-year college grades and persistence. *The Journal of Higher Education*, 79(5), 540–563.
|
|
41
|
+
|
|
42
|
+
Rosenbaum, P. R., & Rubin, D. B. (1983). The central role of the propensity score in observational studies for causal effects. *Biometrika*, 70(1), 41–55. https://doi.org/10.1093/biomet/70.1.41
|
|
43
|
+
|
|
44
|
+
Stephens, N. M., Fryberg, S. A., Markus, H. R., Johnson, C. S., & Covarrubias, R. (2012). Unseen disadvantage: How American universities' focus on independence undermines the academic performance of first-generation college students. *Journal of Personality and Social Psychology*, 102(6), 1178–1197.
|
|
45
|
+
|
|
46
|
+
Strayhorn, T. L. (2019). *College students' sense of belonging* (2nd ed.). Routledge.
|
|
47
|
+
|
|
48
|
+
Tinto, V. (1993). *Leaving college: Rethinking the causes and cures of student attrition* (2nd ed.). University of Chicago Press.
|
|
49
|
+
|
|
50
|
+
Walton, G. M., & Cohen, G. L. (2011). A brief social-belonging intervention improves academic and health outcomes of minority students. *Science*, 331(6023), 1447–1451.
|
|
51
|
+
|
|
52
|
+
Yeagar, D. S. (2016). Teaching a lay theory before college narrows achievement gaps at scale. *PNAS*, 113(24), E3341–E3348.
|
|
53
|
+
|
|
54
|
+
Pascarella, E. T., & Terenzini, P. T. (2005). *How college affects students* (Vol. 2). Jossey-Bass.
|
|
55
|
+
|
|
56
|
+
Rosenbaum, P. R., & Rubin, D. B. (1983). The central role of the propensity score in observational studies for causal effects. *Biometrika*, 70(1), 41–55. doi:10.1093/biomet/70.1.41
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Worked example: a paper before journal submission
|
|
2
|
+
|
|
3
|
+
## Input
|
|
4
|
+
|
|
5
|
+
> *"I'm submitting this to the journal next week ([inputs/paper.md](inputs/paper.md)). Can you
|
|
6
|
+
> check my citations and cross-references? It's APA 7th, and the abstract limit is 50 words."*
|
|
7
|
+
|
|
8
|
+
## What the manuscript shows
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
# Manuscript check
|
|
12
|
+
|
|
13
|
+
Citation style: author-year; 10 references, 9 cited; 2 figures; 1 tables; abstract 46 words.
|
|
14
|
+
|
|
15
|
+
FAIL line 9: cites “Yeager (2016)”, but the reference list spells it Yeagar (2016)
|
|
16
|
+
FAIL line 34: cites “Kuh et al., 2009”, but the reference list has Kuh 2008
|
|
17
|
+
FAIL references on lines 42 and 56 are the same work (DOI 10.1093/biomet/70.1.41)
|
|
18
|
+
FAIL line 11: refers to Section 6, but there is no section 6
|
|
19
|
+
FAIL line 28: refers to Table 2, but there is no Table 2
|
|
20
|
+
FAIL line 28: refers to Figure 3, but there is no Figure 3
|
|
21
|
+
WARN reference on line 54 (Pascarella 2005) is never cited
|
|
22
|
+
WARN Figure 2 (line 30) is never referred to in the text
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Reply
|
|
26
|
+
|
|
27
|
+
**Six things a reviewer would catch:**
|
|
28
|
+
|
|
29
|
+
1. **Line 34: "Kuh et al., 2009"** — your list has Kuh et al. (2008), and the introduction cites
|
|
30
|
+
2008. Change the citation to 2008.
|
|
31
|
+
2. **Line 9: "Yeager (2016)"** — the reference list spells it **Yeagar**. The author is Yeager;
|
|
32
|
+
correct the list entry.
|
|
33
|
+
3. **Rosenbaum and Rubin (1983) is listed twice** (lines 42 and 56, the same DOI). Delete the
|
|
34
|
+
second entry.
|
|
35
|
+
4. **Line 11 refers to Section 6**, but the paper ends at Section 4. Did you mean Section 4?
|
|
36
|
+
5. **Line 28 refers to Table 2 and Figure 3**, but the paper has one table and two figures. Table
|
|
37
|
+
2 looks cut in revision; Figure 3 is probably Figure 2 (retention by cohort).
|
|
38
|
+
6. **Figure 2 is never referred to.** Fixing item 5 to "Figure 2" solves both.
|
|
39
|
+
|
|
40
|
+
**Never cited:** Pascarella and Terenzini (2005). Cite it where the introduction discusses
|
|
41
|
+
retention research, or remove it.
|
|
42
|
+
|
|
43
|
+
**Reference style (APA 7th)**
|
|
44
|
+
- Kuh et al. (2008), Stephens et al. (2012) and Walton and Cohen (2011) have no DOI; APA 7 includes one whenever the work has one, so look each up.
|
|
45
|
+
- Yeager (2016) has one author in your list; the paper has many. APA 7 lists up to 20 authors.
|
|
46
|
+
- "PNAS" should be the full journal name, *Proceedings of the National Academy of Sciences*.
|
|
47
|
+
|
|
48
|
+
**Abstract:** 46 words, under the 50-word limit.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "manuscript-check",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Check a paper, thesis chapter or report before it goes to a journal, a supervisor, examiners or a publisher — a script reads the manuscript and finds every citation with no reference (including near misses: the right author with another year, a misspelt name), references never cited, the same work listed twice, numbered references out of citation order, and Figure, Table and Section references to things that don't exist or are never mentioned, plus footnotes and the abstract's length — then reads the reference list for consistency of style. Use when someone asks to \"check my references\", \"check my citations match the bibliography\", \"proofread my paper before submission\", \"check my figure and table numbering\", \"is my thesis chapter ready to send\", or \"check the cross-references in this report\".",
|
|
5
|
+
"entrypoint": "SKILL.md",
|
|
6
|
+
"deps": {
|
|
7
|
+
"npm": [],
|
|
8
|
+
"pip": []
|
|
9
|
+
},
|
|
10
|
+
"env": {
|
|
11
|
+
"required": [],
|
|
12
|
+
"optional": []
|
|
13
|
+
},
|
|
14
|
+
"related": [
|
|
15
|
+
"research-plan",
|
|
16
|
+
"source-synthesis"
|
|
17
|
+
],
|
|
18
|
+
"tags": [
|
|
19
|
+
"citations",
|
|
20
|
+
"references",
|
|
21
|
+
"manuscript",
|
|
22
|
+
"thesis",
|
|
23
|
+
"academic-writing",
|
|
24
|
+
"cross-references"
|
|
25
|
+
],
|
|
26
|
+
"permissions": {
|
|
27
|
+
"network": [],
|
|
28
|
+
"commands": [],
|
|
29
|
+
"files": "named-paths",
|
|
30
|
+
"notes": "Reads the manuscript it is given and writes nothing."
|
|
31
|
+
},
|
|
32
|
+
"model": {
|
|
33
|
+
"tier": "standard",
|
|
34
|
+
"rationale": "Citation and cross-reference matching come from the script; reading the reference list for style consistency is bounded."
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Check a manuscript's citations and cross-references before it goes to a journal, a
|
|
3
|
+
supervisor or a publisher. Stdlib only.
|
|
4
|
+
|
|
5
|
+
Reads a paper, thesis chapter or report (Markdown, plain text or .docx) and reports, each with
|
|
6
|
+
its line:
|
|
7
|
+
- numbered citations ([3], [2-5], [1, 4]): a number with no reference, a reference never
|
|
8
|
+
cited, references not numbered in order of first citation, a number used twice in the list
|
|
9
|
+
- author-year citations ((Smith, 2020), Smith et al. (2019), (Lee & Park 2021a; Okafor 2018)):
|
|
10
|
+
a citation with no matching reference, a near miss (the right author with another year, or
|
|
11
|
+
the right year with a misspelt name), a reference never cited
|
|
12
|
+
- the same work listed twice (same DOI, or same title)
|
|
13
|
+
- Figure, Table and Section references with no such figure, table or section; a caption never
|
|
14
|
+
referred to; figures or tables numbered out of order or with gaps
|
|
15
|
+
- Markdown footnotes used but not defined, or defined but not used; malformed DOIs
|
|
16
|
+
- the abstract's word count against a limit
|
|
17
|
+
It checks the manuscript's internal consistency, not whether the citations support the claims.
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
python3 manuscript_check.py paper.md [--abstract-limit 250] [--json]
|
|
21
|
+
Exit codes: 0 no failures, 1 a failure, 2 the file can't be read.
|
|
22
|
+
"""
|
|
23
|
+
import argparse
|
|
24
|
+
import json
|
|
25
|
+
import re
|
|
26
|
+
import sys
|
|
27
|
+
import unicodedata
|
|
28
|
+
import zipfile
|
|
29
|
+
from xml.etree import ElementTree
|
|
30
|
+
|
|
31
|
+
W = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
|
|
32
|
+
REF_HEAD = re.compile(r"^\s*(?:#{1,6}\s*)?(?:\*\*)?\s*(?:\d+\.?\s*)?(references|bibliography|works\s+cited|literature\s+cited|reference\s+list|sources)\s*(?:\*\*)?\s*:?\s*$", re.I)
|
|
33
|
+
NUM_CITE = re.compile(r"\[(\d{1,3}(?:\s*[-–,]\s*\d{1,3})*)\]")
|
|
34
|
+
NUM_ENTRY = re.compile(r"^\s*(?:\[(\d{1,3})\]|(\d{1,3})\.)\s+(.+)")
|
|
35
|
+
SURNAME = r"(?:[A-Z][A-Za-z'’\-]+|(?:van|von|de|da|del|der|di|le|la)\s+[A-Z][A-Za-z'’\-]+)"
|
|
36
|
+
AUTH = SURNAME + r"(?:\s+(?:et\s+al\.?|and|&)\s*(?:" + SURNAME + r")?)?"
|
|
37
|
+
YEAR = r"(?:1[89]\d\d|20\d\d)[a-z]?|n\.d\.|in press|forthcoming"
|
|
38
|
+
PAREN = re.compile(r"\(([^()]*?(?:" + YEAR + r")[^()]*)\)")
|
|
39
|
+
NARRATIVE = re.compile(r"\b(" + AUTH + r")\s*\((" + YEAR + r")(?:[,;][^)]*)?\)")
|
|
40
|
+
IN_PAREN = re.compile(r"(?:see\s+|e\.g\.,?\s+|cf\.\s+)?(" + AUTH + r"),?\s+(" + YEAR + r")")
|
|
41
|
+
FIG_REF = re.compile(r"\b(Fig(?:ure|\.)?s?|Tables?|Sections?|§)\s*(\d{1,3}(?:\.\d{1,3})*)(?:\s*(?:and|&|–|-|to)\s*(\d{1,3}(?:\.\d{1,3})*))?", re.I)
|
|
42
|
+
CAPTION = re.compile(r"^\s*(?:!\[[^\]]*\]\([^)]*\)\s*)?(?:\*\*|_|\*)?\s*(Fig(?:ure|\.)?|Table)\s+(\d{1,3})\s*[.:|—–-]", re.I)
|
|
43
|
+
SECTION_HEAD = re.compile(r"^\s*(?:#{1,6}\s*)?(?:\*\*)?(\d{1,2}(?:\.\d{1,2}){0,3})\.?\s+[A-Z]")
|
|
44
|
+
FOOT_USE = re.compile(r"\[\^([^\]\s]+)\](?!:)")
|
|
45
|
+
FOOT_DEF = re.compile(r"^\s*\[\^([^\]\s]+)\]:")
|
|
46
|
+
DOI = re.compile(r"\b(?:doi:\s*|https?://(?:dx\.)?doi\.org/)?(10\.\S+)", re.I)
|
|
47
|
+
GOOD_DOI = re.compile(r"^10\.\d{4,9}/[-._;()/:A-Za-z0-9<>\[\]]+$")
|
|
48
|
+
ABSTRACT = re.compile(r"^\s*(?:#{1,6}\s*)?(?:\*\*)?\s*abstract\s*(?:\*\*)?\s*:?\s*(.*)$", re.I)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def read_lines(path):
|
|
52
|
+
if path.lower().endswith(".docx"):
|
|
53
|
+
with zipfile.ZipFile(path) as z:
|
|
54
|
+
root = ElementTree.fromstring(z.read("word/document.xml"))
|
|
55
|
+
return ["".join(t.text or "" for t in p.iter(W + "t")) for p in root.iter(W + "p")]
|
|
56
|
+
with open(path, encoding="utf-8-sig") as f:
|
|
57
|
+
return f.read().splitlines()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def fold(s):
|
|
61
|
+
"""Lower case without accents, so Müller matches Muller."""
|
|
62
|
+
return "".join(c for c in unicodedata.normalize("NFKD", s) if not unicodedata.combining(c)).lower()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def expand(spec):
|
|
66
|
+
out = []
|
|
67
|
+
for part in re.split(r"\s*,\s*", spec):
|
|
68
|
+
m = re.match(r"(\d+)\s*[-–]\s*(\d+)$", part)
|
|
69
|
+
if m:
|
|
70
|
+
a, b = int(m.group(1)), int(m.group(2))
|
|
71
|
+
out += list(range(a, b + 1)) if 0 < b - a < 200 else [a, b]
|
|
72
|
+
elif part.strip().isdigit():
|
|
73
|
+
out.append(int(part))
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def split(lines):
|
|
78
|
+
"""(body lines with numbers, reference entries) — the list runs from its heading to the next heading."""
|
|
79
|
+
start = next((i for i, l in enumerate(lines) if REF_HEAD.match(l)), None)
|
|
80
|
+
if start is None:
|
|
81
|
+
return list(enumerate(lines, 1)), []
|
|
82
|
+
end = next((i for i in range(start + 1, len(lines)) if re.match(r"^\s*#{1,6}\s+\S", lines[i]) and not REF_HEAD.match(lines[i])
|
|
83
|
+
and not re.match(r"^\s*#{1,6}\s+(appendix|supplement)", lines[i], re.I) or
|
|
84
|
+
re.match(r"^\s*#{1,6}\s+(appendix|supplement)", lines[i], re.I)), len(lines))
|
|
85
|
+
body = list(enumerate(lines[:start], 1)) + list(enumerate(lines[end:], end + 1))
|
|
86
|
+
entries, cur = [], None
|
|
87
|
+
for n, l in enumerate(lines[start + 1:end], start + 2):
|
|
88
|
+
if not l.strip():
|
|
89
|
+
cur = None
|
|
90
|
+
continue
|
|
91
|
+
m = NUM_ENTRY.match(l)
|
|
92
|
+
bullet = re.match(r"^\s*[-*•]\s+(.+)", l)
|
|
93
|
+
if m or bullet or cur is None:
|
|
94
|
+
text = m.group(3) if m else bullet.group(1) if bullet else l.strip()
|
|
95
|
+
cur = {"line": n, "num": int(m.group(1) or m.group(2)) if m else None, "text": text.strip()}
|
|
96
|
+
entries.append(cur)
|
|
97
|
+
else:
|
|
98
|
+
cur["text"] += " " + l.strip()
|
|
99
|
+
return body, entries
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def entry_key(e):
|
|
103
|
+
"""(first author's surname, year with suffix) from an author-year reference entry."""
|
|
104
|
+
t = re.sub(r"^[\s*_]+", "", e["text"])
|
|
105
|
+
m = re.match(r"((?:van|von|de|da|del|der|di|le|la)\s+)?([A-Z][^\s,.(]*)", t)
|
|
106
|
+
y = re.search(r"\((" + YEAR + r")\)|[,.]\s*(" + YEAR + r")[.,)]", t)
|
|
107
|
+
sur = ((m.group(1) or "") + m.group(2)).strip() if m else None
|
|
108
|
+
year = (y.group(1) or y.group(2)) if y else None
|
|
109
|
+
return (fold(sur) if sur else None, year.lower() if year else None)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def surname_of(auth):
|
|
113
|
+
a = re.split(r"\s+(?:et\s+al\.?|and|&)\s*", auth.strip())[0]
|
|
114
|
+
return fold(a.strip())
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def author_year_cites(body):
|
|
118
|
+
cites = []
|
|
119
|
+
for n, line in body:
|
|
120
|
+
clean = re.sub(r"\[[^\]]*\]\([^)]*\)", "", line)
|
|
121
|
+
for m in NARRATIVE.finditer(clean):
|
|
122
|
+
cites.append((n, surname_of(m.group(1)), m.group(2).lower(), m.group(0)))
|
|
123
|
+
for p in PAREN.finditer(clean):
|
|
124
|
+
inner = p.group(1)
|
|
125
|
+
for chunk in re.split(r";", inner):
|
|
126
|
+
mm = IN_PAREN.search(chunk)
|
|
127
|
+
if mm:
|
|
128
|
+
years = re.findall(YEAR, chunk[mm.start(2):])
|
|
129
|
+
for y in years or [mm.group(2)]:
|
|
130
|
+
cites.append((n, surname_of(mm.group(1)), y.lower(), chunk.strip()))
|
|
131
|
+
seen, out = set(), []
|
|
132
|
+
for c in cites:
|
|
133
|
+
if (c[0], c[1], c[2]) not in seen:
|
|
134
|
+
seen.add((c[0], c[1], c[2]))
|
|
135
|
+
out.append(c)
|
|
136
|
+
return out
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def close(a, b):
|
|
140
|
+
"""Two surnames one edit apart (a typo), or one a prefix of the other."""
|
|
141
|
+
if a == b:
|
|
142
|
+
return True
|
|
143
|
+
if abs(len(a) - len(b)) > 1:
|
|
144
|
+
return False
|
|
145
|
+
if len(a) == len(b):
|
|
146
|
+
return sum(x != y for x, y in zip(a, b)) == 1
|
|
147
|
+
s, l = (a, b) if len(a) < len(b) else (b, a)
|
|
148
|
+
return any(l[:i] + l[i + 1:] == s for i in range(len(l)))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def check(lines, abstract_limit=None):
|
|
152
|
+
fails, warns = [], []
|
|
153
|
+
body, entries = split(lines)
|
|
154
|
+
numbered = [e for e in entries if e["num"] is not None]
|
|
155
|
+
style = "numbered" if numbered and len(numbered) >= len(entries) / 2 else "author-year" if entries else None
|
|
156
|
+
body_text = [(n, l) for n, l in body]
|
|
157
|
+
stats = {"references": len(entries), "style": style}
|
|
158
|
+
if style == "numbered":
|
|
159
|
+
nums = {}
|
|
160
|
+
for e in numbered:
|
|
161
|
+
if e["num"] in nums:
|
|
162
|
+
fails.append(f"reference [{e['num']}] is numbered twice in the list (lines {nums[e['num']]} and {e['line']})")
|
|
163
|
+
nums.setdefault(e["num"], e["line"])
|
|
164
|
+
order, cited = [], {}
|
|
165
|
+
for n, l in body_text:
|
|
166
|
+
for m in NUM_CITE.finditer(l):
|
|
167
|
+
if re.search(r"\]\(", l[m.end():m.end() + 2]):
|
|
168
|
+
continue # a Markdown link, not a citation
|
|
169
|
+
for k in expand(m.group(1)):
|
|
170
|
+
cited.setdefault(k, n)
|
|
171
|
+
if k not in order:
|
|
172
|
+
order.append(k)
|
|
173
|
+
for k, n in sorted(cited.items(), key=lambda x: x[1]):
|
|
174
|
+
if k not in nums:
|
|
175
|
+
fails.append(f"line {n}: cites [{k}], but there is no reference [{k}]")
|
|
176
|
+
for k, n in sorted(nums.items()):
|
|
177
|
+
if k not in cited:
|
|
178
|
+
warns.append(f"reference [{k}] (line {n}) is never cited")
|
|
179
|
+
listed = sorted(nums)
|
|
180
|
+
for a, b in zip(listed, listed[1:]):
|
|
181
|
+
if b != a + 1:
|
|
182
|
+
warns.append(f"the reference list jumps from [{a}] to [{b}]")
|
|
183
|
+
in_list = [k for k in order if k in nums]
|
|
184
|
+
if in_list and in_list != sorted(in_list):
|
|
185
|
+
# the numbering a citation-order style expects: cited references by first citation,
|
|
186
|
+
# then any never cited, in their current order
|
|
187
|
+
new_order = in_list + [k for k in sorted(nums) if k not in in_list]
|
|
188
|
+
stats["renumber"] = {old: new for new, old in enumerate(new_order, 1) if old != new}
|
|
189
|
+
first = next(k for i, k in enumerate(in_list) if k != sorted(in_list)[i])
|
|
190
|
+
warns.append(f"references aren't numbered in order of first citation (from [{first}], line {cited[first]}); "
|
|
191
|
+
"numbered styles such as Vancouver and IEEE expect that")
|
|
192
|
+
stats["cited"] = len([k for k in cited if k in nums])
|
|
193
|
+
elif style == "author-year":
|
|
194
|
+
keys = [(entry_key(e), e) for e in entries]
|
|
195
|
+
cites = author_year_cites(body_text)
|
|
196
|
+
used = set()
|
|
197
|
+
for n, sur, year, text in cites:
|
|
198
|
+
exact = [e for (s, y), e in keys if s == sur and y == year]
|
|
199
|
+
if exact:
|
|
200
|
+
used.update(id(e) for e in exact)
|
|
201
|
+
continue
|
|
202
|
+
if year.rstrip("abcdefgh") == year and any(s == sur and y and y.rstrip("abcdefgh") == year for (s, y), _ in keys):
|
|
203
|
+
warns.append(f"line {n}: “{text}” needs its year's letter: the list has more than one {sur.title()} {year}")
|
|
204
|
+
used.update(id(e) for (s, y), e in keys if s == sur and y and y.startswith(year))
|
|
205
|
+
continue
|
|
206
|
+
same_author = [y for (s, y), _ in keys if s == sur and y]
|
|
207
|
+
typo = [s for (s, y), _ in keys if s and y == year and close(s, sur)]
|
|
208
|
+
if same_author:
|
|
209
|
+
fails.append(f"line {n}: cites “{text}”, but the reference list has {sur.title()} {', '.join(sorted(set(same_author)))}")
|
|
210
|
+
used.update(id(e) for (s, y), e in keys if s == sur)
|
|
211
|
+
elif typo:
|
|
212
|
+
fails.append(f"line {n}: cites “{text}”, but the reference list spells it {typo[0].title()} ({year})")
|
|
213
|
+
used.update(id(e) for (s, y), e in keys if s == typo[0] and y == year)
|
|
214
|
+
else:
|
|
215
|
+
fails.append(f"line {n}: cites “{text}”, but there is no {sur.title()} {year} in the reference list")
|
|
216
|
+
for (s, y), e in keys:
|
|
217
|
+
if id(e) not in used and s:
|
|
218
|
+
warns.append(f"reference on line {e['line']} ({s.title()} {y or 'no year'}) is never cited")
|
|
219
|
+
stats["cited"] = len(used)
|
|
220
|
+
elif any(NUM_CITE.search(l) for _, l in body_text):
|
|
221
|
+
fails.append("the text cites references, but there is no reference list (a heading such as References or Bibliography)")
|
|
222
|
+
# the same work twice
|
|
223
|
+
seen_doi, seen_title, dup_pairs = {}, {}, set()
|
|
224
|
+
for e in entries:
|
|
225
|
+
for m in DOI.finditer(e["text"]):
|
|
226
|
+
d = m.group(1).rstrip(".,;)").lower()
|
|
227
|
+
if not GOOD_DOI.match(d):
|
|
228
|
+
warns.append(f"reference on line {e['line']}: “{m.group(0)[:60]}” isn't a well-formed DOI")
|
|
229
|
+
elif d in seen_doi:
|
|
230
|
+
fails.append(f"references on lines {seen_doi[d]} and {e['line']} are the same work (DOI {d})")
|
|
231
|
+
dup_pairs.add((seen_doi[d], e["line"]))
|
|
232
|
+
seen_doi.setdefault(d, e["line"])
|
|
233
|
+
t = re.search(r"(?:\)\.?|\d{4}[a-z]?\.)\s*[\"“]?([^.\"”]{20,})[\"”]?[.?]", e["text"])
|
|
234
|
+
if not t and e["num"] is not None:
|
|
235
|
+
# Vancouver and IEEE put the title after the authors: "Smith J, Lee K. Title of the work. Journal."
|
|
236
|
+
t = re.search(r"^[^.]*?(?:et al|[A-Z]{1,3})\.\s+[\"“]?([^.\"”]{20,})[\"”]?[.?]", e["text"])
|
|
237
|
+
if t:
|
|
238
|
+
key = re.sub(r"[^a-z0-9]+", " ", fold(t.group(1))).strip()
|
|
239
|
+
if key in seen_title and (seen_title[key], e["line"]) not in dup_pairs:
|
|
240
|
+
fails.append(f"references on lines {seen_title[key]} and {e['line']} have the same title")
|
|
241
|
+
seen_title.setdefault(key, e["line"])
|
|
242
|
+
# figures, tables, sections
|
|
243
|
+
captions = {"figure": {}, "table": {}}
|
|
244
|
+
sections = set()
|
|
245
|
+
for n, l in body_text:
|
|
246
|
+
c = CAPTION.match(l)
|
|
247
|
+
if c:
|
|
248
|
+
kind = "table" if c.group(1).lower().startswith("tab") else "figure"
|
|
249
|
+
k = int(c.group(2))
|
|
250
|
+
if k in captions[kind]:
|
|
251
|
+
fails.append(f"line {n}: {kind.title()} {k} has a second caption (also line {captions[kind][k]})")
|
|
252
|
+
captions[kind].setdefault(k, n)
|
|
253
|
+
h = SECTION_HEAD.match(l)
|
|
254
|
+
if h:
|
|
255
|
+
sections.add(h.group(1))
|
|
256
|
+
refs = {"figure": {}, "table": {}}
|
|
257
|
+
for n, l in body_text:
|
|
258
|
+
if CAPTION.match(l):
|
|
259
|
+
l = CAPTION.sub("", l, count=1)
|
|
260
|
+
for m in FIG_REF.finditer(l):
|
|
261
|
+
word = m.group(1).lower()
|
|
262
|
+
kind = "table" if word.startswith("tab") else "section" if word.startswith(("sec", "§")) else "figure"
|
|
263
|
+
nums = [m.group(2)] + ([m.group(3)] if m.group(3) else [])
|
|
264
|
+
if kind != "section" and m.group(3) and re.search(r"–|-|to", m.group(0)):
|
|
265
|
+
try:
|
|
266
|
+
nums = [str(x) for x in range(int(m.group(2)), int(m.group(3)) + 1)]
|
|
267
|
+
except ValueError:
|
|
268
|
+
pass
|
|
269
|
+
for k in nums:
|
|
270
|
+
if kind == "section":
|
|
271
|
+
if sections and k not in sections and not any(s.startswith(k + ".") for s in sections):
|
|
272
|
+
fails.append(f"line {n}: refers to Section {k}, but there is no section {k}")
|
|
273
|
+
continue
|
|
274
|
+
if "." in k:
|
|
275
|
+
continue
|
|
276
|
+
refs[kind].setdefault(int(k), n)
|
|
277
|
+
if captions[kind] and int(k) not in captions[kind]:
|
|
278
|
+
fails.append(f"line {n}: refers to {kind.title()} {k}, but there is no {kind.title()} {k}")
|
|
279
|
+
for kind in ("figure", "table"):
|
|
280
|
+
nums = sorted(captions[kind])
|
|
281
|
+
for k in nums:
|
|
282
|
+
if k not in refs[kind]:
|
|
283
|
+
warns.append(f"{kind.title()} {k} (line {captions[kind][k]}) is never referred to in the text")
|
|
284
|
+
for a, b in zip(nums, nums[1:]):
|
|
285
|
+
if b != a + 1:
|
|
286
|
+
warns.append(f"{kind}s jump from {a} to {b}")
|
|
287
|
+
if nums and nums[0] != 1:
|
|
288
|
+
warns.append(f"the first {kind} is numbered {nums[0]}")
|
|
289
|
+
first_mention = [k for k, _ in sorted(refs[kind].items(), key=lambda x: x[1]) if k in captions[kind]]
|
|
290
|
+
if first_mention != sorted(first_mention):
|
|
291
|
+
warns.append(f"{kind}s are first mentioned out of order ({', '.join(map(str, first_mention))})")
|
|
292
|
+
stats[kind + "s"] = len(nums)
|
|
293
|
+
# footnotes
|
|
294
|
+
uses, defs = {}, {}
|
|
295
|
+
for n, l in enumerate(lines, 1):
|
|
296
|
+
d = FOOT_DEF.match(l)
|
|
297
|
+
if d:
|
|
298
|
+
defs.setdefault(d.group(1), n)
|
|
299
|
+
for u in FOOT_USE.finditer(l):
|
|
300
|
+
uses.setdefault(u.group(1), n)
|
|
301
|
+
for k, n in uses.items():
|
|
302
|
+
if k not in defs:
|
|
303
|
+
fails.append(f"line {n}: footnote [^{k}] is used but never defined")
|
|
304
|
+
for k, n in defs.items():
|
|
305
|
+
if k not in uses:
|
|
306
|
+
warns.append(f"footnote [^{k}] (line {n}) is defined but never used")
|
|
307
|
+
# abstract
|
|
308
|
+
abstract_words = None
|
|
309
|
+
for i, l in enumerate(lines):
|
|
310
|
+
m = ABSTRACT.match(l)
|
|
311
|
+
if m:
|
|
312
|
+
words, j = len(re.findall(r"\b[\w'’-]+\b", m.group(1))), i + 1
|
|
313
|
+
while j < len(lines) and not re.match(r"^\s*(#{1,6}\s|\*\*[^*]+\*\*\s*$|(keywords|key words)\s*:)", lines[j], re.I):
|
|
314
|
+
words += len(re.findall(r"\b[\w'’-]+\b", lines[j]))
|
|
315
|
+
j += 1
|
|
316
|
+
abstract_words = words
|
|
317
|
+
break
|
|
318
|
+
if abstract_limit and abstract_words is not None and abstract_words > abstract_limit:
|
|
319
|
+
fails.append(f"the abstract is {abstract_words} words, over the limit of {abstract_limit}")
|
|
320
|
+
stats["abstract_words"] = abstract_words
|
|
321
|
+
return {"fails": fails, "warns": warns, "stats": stats}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def report(r):
|
|
325
|
+
s = r["stats"]
|
|
326
|
+
L = ["# Manuscript check", "",
|
|
327
|
+
f"Citation style: {s['style'] or 'none found'}; {s['references']} references"
|
|
328
|
+
+ (f", {s.get('cited', 0)} cited" if s["style"] else "") + f"; {s.get('figures', 0)} figures; {s.get('tables', 0)} tables"
|
|
329
|
+
+ (f"; abstract {s['abstract_words']} words" if s.get("abstract_words") is not None else "") + ".", ""]
|
|
330
|
+
if s.get("renumber"):
|
|
331
|
+
L += ["Renumbering for citation order (old → new): " + ", ".join(f"[{o}] → [{n}]" for o, n in sorted(s["renumber"].items())), ""]
|
|
332
|
+
if not r["fails"]:
|
|
333
|
+
L.append("PASS every citation has a reference, and every figure, table and section referred to exists.")
|
|
334
|
+
L += [f"FAIL {f}" for f in r["fails"]] + [f"WARN {w}" for w in r["warns"]]
|
|
335
|
+
return "\n".join(L) + "\n"
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def main(argv=None):
|
|
339
|
+
ap = argparse.ArgumentParser(description="Check a manuscript's citations and cross-references.")
|
|
340
|
+
ap.add_argument("manuscript")
|
|
341
|
+
ap.add_argument("--abstract-limit", type=int)
|
|
342
|
+
ap.add_argument("--json", action="store_true")
|
|
343
|
+
args = ap.parse_args(argv)
|
|
344
|
+
try:
|
|
345
|
+
lines = read_lines(args.manuscript)
|
|
346
|
+
except (OSError, UnicodeDecodeError, zipfile.BadZipFile, KeyError, ElementTree.ParseError) as e:
|
|
347
|
+
print(f"FAIL cannot read {args.manuscript}: {e}")
|
|
348
|
+
return 2
|
|
349
|
+
r = check(lines, args.abstract_limit)
|
|
350
|
+
print(json.dumps(r, indent=2) if args.json else report(r), end="" if not args.json else "\n")
|
|
351
|
+
return 1 if r["fails"] else 0
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
if __name__ == "__main__":
|
|
355
|
+
sys.exit(main())
|