skilldrop-cli 1.11.1 → 1.13.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/bin/skilldrop.js +46 -3
- package/catalogue.json +2 -1
- package/model-routing.json +4 -0
- package/package.json +1 -1
- package/packs/ai-engineering/skills/agent-config-review/evals/files/1/dot.gitignore +5 -0
- package/packs/ai-engineering/skills/agent-config-review/examples/inputs/tidewater-ingest/dot.gitignore +3 -0
- package/packs/ai-engineering/skills/agent-config-review/examples/python-repo-tighten.md +3 -3
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/dot.npmrc +3 -0
- package/packs/experience-design/skills/locale-check/SKILL.md +1 -1
- package/packs/experience-design/skills/locale-check/manifest.json +2 -2
- package/packs/frontend/skills/bundle-size-review/evals/files/9/dot.gitignore +5 -0
- 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/packs/ai-engineering/skills/agent-config-review/evals/files/1/{.claude → dot.claude}/agents/db-migrator.md +0 -0
- /package/packs/ai-engineering/skills/agent-config-review/evals/files/1/{.claude → dot.claude}/agents/reviewer.md +0 -0
- /package/packs/ai-engineering/skills/agent-config-review/evals/files/1/{.claude → dot.claude}/settings.json +0 -0
- /package/packs/ai-engineering/skills/agent-config-review/evals/files/1/{.claude → dot.claude}/settings.local.json +0 -0
- /package/packs/ai-engineering/skills/agent-config-review/evals/files/1/{.mcp.json → dot.mcp.json} +0 -0
- /package/packs/ai-engineering/skills/agent-config-review/evals/files/2/{.cursor → dot.cursor}/mcp.json +0 -0
- /package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/{.claude → dot.claude}/agents/release-notes.md +0 -0
- /package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/{.claude → dot.claude}/settings.json +0 -0
- /package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/{.mcp.json → dot.mcp.json} +0 -0
- /package/packs/ai-engineering/skills/agent-config-review/evals/files/5/{.github → dot.github}/workflows/codex-triage.yml.fixture +0 -0
- /package/packs/ai-engineering/skills/agent-config-review/examples/inputs/tidewater-ingest/{.claude → dot.claude}/settings.json +0 -0
- /package/packs/ai-engineering/skills/agent-config-review/examples/inputs/tidewater-ingest/{.mcp.json → dot.mcp.json} +0 -0
- /package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/{.mcp.json → dot.mcp.json} +0 -0
- /package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
- /package/packs/ai-engineering/skills/mcp-pairing/evals/files/4/{.mcp.json → dot.mcp.json} +0 -0
- /package/packs/api-builder/skills/eval-harness-generator/evals/files/1/{SKILL.md → SKILL.md.fixture} +0 -0
- /package/packs/api-builder/skills/eval-harness-generator/evals/files/2/{SKILL.md → SKILL.md.fixture} +0 -0
- /package/packs/core/skills/context-graph/evals/files/9/{.env.example → dot.env.example} +0 -0
- /package/packs/dev-team/skills/agents-md-generator/evals/files/1/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
- /package/packs/dev-team/skills/agents-md-generator/evals/files/5/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
- /package/packs/dev-team/skills/codebase-tour/evals/files/1/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
- /package/packs/dev-team/skills/codebase-tour/evals/files/2/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
- /package/packs/dev-team/skills/codebase-tour/evals/files/3/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
- /package/packs/dev-team/skills/codebase-tour/evals/files/4/{.env.example → dot.env.example} +0 -0
- /package/packs/dev-team/skills/codebase-tour/evals/files/4/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
- /package/packs/dev-team/skills/codebase-tour/evals/files/5/{.env.example → dot.env.example} +0 -0
- /package/packs/dev-team/skills/codebase-tour/evals/files/5/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
- /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
- /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
- /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/{.github → dot.github}/workflows/release.yml.fixture +0 -0
- /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
- /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
- /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/{.github → dot.github}/workflows/release.yml.fixture +0 -0
- /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/{.nvmrc → dot.nvmrc} +0 -0
- /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
- /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/{.github → dot.github}/workflows/integration.yml.fixture +0 -0
- /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
- /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/{.lintwright.json → dot.lintwright.json} +0 -0
- /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/{.nvmrc → dot.nvmrc} +0 -0
- /package/packs/dev-team/skills/sonar-onboard/evals/files/2/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
- /package/packs/dev-team/skills/sonar-onboard/evals/files/2/{.gitlab-ci.yml → dot.gitlab-ci.yml} +0 -0
- /package/packs/frontend/skills/bundle-size-review/evals/files/5/{.browserslistrc → dot.browserslistrc} +0 -0
- /package/packs/frontend/skills/bundle-size-review/evals/files/6/{.browserslistrc → dot.browserslistrc} +0 -0
- /package/packs/frontend/skills/bundle-size-review/evals/files/8/{.browserslistrc → dot.browserslistrc} +0 -0
- /package/packs/frontend/skills/bundle-size-review/evals/files/9/{.github → dot.github}/workflows/size.yml.fixture +0 -0
- /package/packs/skill-engineering/skills/skill-review/evals/files/1/skills/changelog-writer/{SKILL.md → SKILL.md.fixture} +0 -0
- /package/packs/skill-engineering/skills/skill-review/evals/files/1/skills/pr-summary/{SKILL.md → SKILL.md.fixture} +0 -0
- /package/packs/skill-engineering/skills/skill-review/evals/files/2/invoice-chaser/{SKILL.md → SKILL.md.fixture} +0 -0
|
@@ -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())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/packs/ai-engineering/skills/agent-config-review/evals/files/1/{.mcp.json → dot.mcp.json}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/packs/api-builder/skills/eval-harness-generator/evals/files/1/{SKILL.md → SKILL.md.fixture}
RENAMED
|
File without changes
|
/package/packs/api-builder/skills/eval-harness-generator/evals/files/2/{SKILL.md → SKILL.md.fixture}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/packs/dev-team/skills/sonar-onboard/evals/files/2/{.gitlab-ci.yml → dot.gitlab-ci.yml}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|