touchneedle 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- touchneedle-0.1.0/LICENSE +21 -0
- touchneedle-0.1.0/PKG-INFO +146 -0
- touchneedle-0.1.0/README.md +119 -0
- touchneedle-0.1.0/pyproject.toml +77 -0
- touchneedle-0.1.0/scripts/touchneedle.egg-info/PKG-INFO +146 -0
- touchneedle-0.1.0/scripts/touchneedle.egg-info/SOURCES.txt +13 -0
- touchneedle-0.1.0/scripts/touchneedle.egg-info/dependency_links.txt +1 -0
- touchneedle-0.1.0/scripts/touchneedle.egg-info/entry_points.txt +2 -0
- touchneedle-0.1.0/scripts/touchneedle.egg-info/top_level.txt +1 -0
- touchneedle-0.1.0/scripts/touchneedle.py +911 -0
- touchneedle-0.1.0/setup.cfg +4 -0
- touchneedle-0.1.0/tests/test_citations.py +150 -0
- touchneedle-0.1.0/tests/test_cli.py +149 -0
- touchneedle-0.1.0/tests/test_parsing.py +179 -0
- touchneedle-0.1.0/tests/test_verify.py +192 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ncoleman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: touchneedle
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Verify that every citation and reference in a document is real, accurately described, and consistently used.
|
|
5
|
+
Author: ncoleman
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nicoleman0/touchneedle
|
|
8
|
+
Project-URL: Source, https://github.com/nicoleman0/touchneedle
|
|
9
|
+
Project-URL: Changelog, https://github.com/nicoleman0/touchneedle/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/nicoleman0/touchneedle/issues
|
|
11
|
+
Keywords: citations,references,bibliography,academic,fact-checking
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Text Processing :: Markup
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# touchneedle
|
|
29
|
+
|
|
30
|
+
Citiation tool that verifies the citations in a document are real,
|
|
31
|
+
accurately described, and consistently used.
|
|
32
|
+
|
|
33
|
+
Useful for both students and examiners who wish to corroborate citations.
|
|
34
|
+
|
|
35
|
+
It works standalone, or as a coding agent skill.
|
|
36
|
+
|
|
37
|
+
Most commercial citation checkers want a `.bib` file and check it against academic
|
|
38
|
+
databases. That covers journal articles but misses standards,
|
|
39
|
+
specifications, vendor documentation, and blog posts. In a lot of real
|
|
40
|
+
bibliographies, this is half the list.
|
|
41
|
+
|
|
42
|
+
So this tool parses a **prose reference list** (Harvard/author-date)
|
|
43
|
+
straight out of Markdown or `.docx`, and routes each entry to whichever
|
|
44
|
+
authority can actually confirm it.
|
|
45
|
+
|
|
46
|
+
## What it checks
|
|
47
|
+
|
|
48
|
+
**Existence and metadata** — scripted and deterministic:
|
|
49
|
+
|
|
50
|
+
| Entry carries | Checked against |
|
|
51
|
+
|---|---|
|
|
52
|
+
| arXiv id | arXiv API |
|
|
53
|
+
| DOI | Crossref |
|
|
54
|
+
| RFC number | IETF datatracker, falling back to rfc-editor |
|
|
55
|
+
| `draft-*` name | IETF datatracker, **including whether the cited revision is still current** |
|
|
56
|
+
| Quoted title in an academic venue | Crossref, then OpenAlex, by title |
|
|
57
|
+
| A URL and nothing else | Fetched live; page title compared with the cited title |
|
|
58
|
+
|
|
59
|
+
Entries with both an identifier and a URL get both, so a real paper behind a dead
|
|
60
|
+
link is still reported. Detects the fabricated-citation signature — a real title
|
|
61
|
+
carrying the wrong authors — as `MISMATCH`.
|
|
62
|
+
|
|
63
|
+
**Internal consistency** — every in-text citation resolves to a list entry, every
|
|
64
|
+
list entry is cited somewhere, and `2025a`/`2025b` suffixes are used unambiguously.
|
|
65
|
+
|
|
66
|
+
**Claim support** — the pass that needs reading rather than fetching. `claims`
|
|
67
|
+
emits a worklist pairing each in-text citation with the sentence making the claim
|
|
68
|
+
and a locator for the source; the model then reads each source and rules
|
|
69
|
+
SUPPORTED / PARTIAL / UNSUPPORTED / INACCESSIBLE. This catches the failure the
|
|
70
|
+
database checks cannot: a genuine source attached to a claim it does not make.
|
|
71
|
+
|
|
72
|
+
## Install
|
|
73
|
+
|
|
74
|
+
As a command-line tool:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install touchneedle
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
As a Claude Code skill:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
git clone https://github.com/nicoleman0/touchneedle ~/.claude/skills/touchneedle
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Or as a Claude Code plugin:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
/plugin marketplace add nicoleman0/touchneedle
|
|
90
|
+
/plugin install touchneedle
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
There are no dependencies beyond Python 3.11+. `pandoc` is needed only for `.docx` input.
|
|
94
|
+
|
|
95
|
+
Then, in Claude Code: *"check the citations in thesis.docx"*.
|
|
96
|
+
|
|
97
|
+
## Use directly
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
touchneedle check thesis.docx --out report.md --json data.json
|
|
101
|
+
touchneedle claims thesis.docx --out claims.md
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
From a clone, without installing, that is `python3 scripts/touchneedle.py …` —
|
|
105
|
+
the same file either way.
|
|
106
|
+
|
|
107
|
+
Options: `--offline` (parse and cross-check only, no network), `--cache DIR`
|
|
108
|
+
(HTTP cache, 7-day TTL, so re-runs are nearly free), `--timeout N`, and
|
|
109
|
+
`--mailto you@example.com` for Crossref and OpenAlex's polite rate-limit pool.
|
|
110
|
+
`--mailto` is off by default and never inferred — it sends an address to third
|
|
111
|
+
parties.
|
|
112
|
+
|
|
113
|
+
`check` exits 2 when something needs attention, 0 when clean, so it drops into CI.
|
|
114
|
+
|
|
115
|
+
## Statuses
|
|
116
|
+
|
|
117
|
+
`MISMATCH` and `NOT_FOUND` are the ones that damage a submission. `LINK_DEAD` and
|
|
118
|
+
`STALE` need a fix but not a retraction. `PARTIAL`, `LINK_MOVED` and
|
|
119
|
+
`UNVERIFIABLE` are for a glance — notably, PDFs and JS-rendered pages land in
|
|
120
|
+
`PARTIAL` routinely, because no `<title>` can be read from them. A `PARTIAL` is a
|
|
121
|
+
limit of the check, not evidence against the citation.
|
|
122
|
+
|
|
123
|
+
## Limits
|
|
124
|
+
|
|
125
|
+
Author-date reference lists only — numeric styles (Vancouver, IEEE) are not
|
|
126
|
+
parsed. Page numbers, edition and publisher details are not checked.
|
|
127
|
+
|
|
128
|
+
Sources behind paywalls cannot be verified beyond their metadata record.
|
|
129
|
+
|
|
130
|
+
The list of in-text citations with no matching entry has expected false positives,
|
|
131
|
+
because a regex cannot distinguish `(Smith, 2024)` from `(ICLR 2023)`.
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
python3 -m unittest discover -s tests -t tests
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request.
|
|
140
|
+
|
|
141
|
+
The short version: standard library only, tests stay offline, and never let a coverage gap
|
|
142
|
+
report itself as a finding.
|
|
143
|
+
|
|
144
|
+
## Licence
|
|
145
|
+
|
|
146
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# touchneedle
|
|
2
|
+
|
|
3
|
+
Citiation tool that verifies the citations in a document are real,
|
|
4
|
+
accurately described, and consistently used.
|
|
5
|
+
|
|
6
|
+
Useful for both students and examiners who wish to corroborate citations.
|
|
7
|
+
|
|
8
|
+
It works standalone, or as a coding agent skill.
|
|
9
|
+
|
|
10
|
+
Most commercial citation checkers want a `.bib` file and check it against academic
|
|
11
|
+
databases. That covers journal articles but misses standards,
|
|
12
|
+
specifications, vendor documentation, and blog posts. In a lot of real
|
|
13
|
+
bibliographies, this is half the list.
|
|
14
|
+
|
|
15
|
+
So this tool parses a **prose reference list** (Harvard/author-date)
|
|
16
|
+
straight out of Markdown or `.docx`, and routes each entry to whichever
|
|
17
|
+
authority can actually confirm it.
|
|
18
|
+
|
|
19
|
+
## What it checks
|
|
20
|
+
|
|
21
|
+
**Existence and metadata** — scripted and deterministic:
|
|
22
|
+
|
|
23
|
+
| Entry carries | Checked against |
|
|
24
|
+
|---|---|
|
|
25
|
+
| arXiv id | arXiv API |
|
|
26
|
+
| DOI | Crossref |
|
|
27
|
+
| RFC number | IETF datatracker, falling back to rfc-editor |
|
|
28
|
+
| `draft-*` name | IETF datatracker, **including whether the cited revision is still current** |
|
|
29
|
+
| Quoted title in an academic venue | Crossref, then OpenAlex, by title |
|
|
30
|
+
| A URL and nothing else | Fetched live; page title compared with the cited title |
|
|
31
|
+
|
|
32
|
+
Entries with both an identifier and a URL get both, so a real paper behind a dead
|
|
33
|
+
link is still reported. Detects the fabricated-citation signature — a real title
|
|
34
|
+
carrying the wrong authors — as `MISMATCH`.
|
|
35
|
+
|
|
36
|
+
**Internal consistency** — every in-text citation resolves to a list entry, every
|
|
37
|
+
list entry is cited somewhere, and `2025a`/`2025b` suffixes are used unambiguously.
|
|
38
|
+
|
|
39
|
+
**Claim support** — the pass that needs reading rather than fetching. `claims`
|
|
40
|
+
emits a worklist pairing each in-text citation with the sentence making the claim
|
|
41
|
+
and a locator for the source; the model then reads each source and rules
|
|
42
|
+
SUPPORTED / PARTIAL / UNSUPPORTED / INACCESSIBLE. This catches the failure the
|
|
43
|
+
database checks cannot: a genuine source attached to a claim it does not make.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
As a command-line tool:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install touchneedle
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
As a Claude Code skill:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
git clone https://github.com/nicoleman0/touchneedle ~/.claude/skills/touchneedle
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Or as a Claude Code plugin:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
/plugin marketplace add nicoleman0/touchneedle
|
|
63
|
+
/plugin install touchneedle
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
There are no dependencies beyond Python 3.11+. `pandoc` is needed only for `.docx` input.
|
|
67
|
+
|
|
68
|
+
Then, in Claude Code: *"check the citations in thesis.docx"*.
|
|
69
|
+
|
|
70
|
+
## Use directly
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
touchneedle check thesis.docx --out report.md --json data.json
|
|
74
|
+
touchneedle claims thesis.docx --out claims.md
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
From a clone, without installing, that is `python3 scripts/touchneedle.py …` —
|
|
78
|
+
the same file either way.
|
|
79
|
+
|
|
80
|
+
Options: `--offline` (parse and cross-check only, no network), `--cache DIR`
|
|
81
|
+
(HTTP cache, 7-day TTL, so re-runs are nearly free), `--timeout N`, and
|
|
82
|
+
`--mailto you@example.com` for Crossref and OpenAlex's polite rate-limit pool.
|
|
83
|
+
`--mailto` is off by default and never inferred — it sends an address to third
|
|
84
|
+
parties.
|
|
85
|
+
|
|
86
|
+
`check` exits 2 when something needs attention, 0 when clean, so it drops into CI.
|
|
87
|
+
|
|
88
|
+
## Statuses
|
|
89
|
+
|
|
90
|
+
`MISMATCH` and `NOT_FOUND` are the ones that damage a submission. `LINK_DEAD` and
|
|
91
|
+
`STALE` need a fix but not a retraction. `PARTIAL`, `LINK_MOVED` and
|
|
92
|
+
`UNVERIFIABLE` are for a glance — notably, PDFs and JS-rendered pages land in
|
|
93
|
+
`PARTIAL` routinely, because no `<title>` can be read from them. A `PARTIAL` is a
|
|
94
|
+
limit of the check, not evidence against the citation.
|
|
95
|
+
|
|
96
|
+
## Limits
|
|
97
|
+
|
|
98
|
+
Author-date reference lists only — numeric styles (Vancouver, IEEE) are not
|
|
99
|
+
parsed. Page numbers, edition and publisher details are not checked.
|
|
100
|
+
|
|
101
|
+
Sources behind paywalls cannot be verified beyond their metadata record.
|
|
102
|
+
|
|
103
|
+
The list of in-text citations with no matching entry has expected false positives,
|
|
104
|
+
because a regex cannot distinguish `(Smith, 2024)` from `(ICLR 2023)`.
|
|
105
|
+
|
|
106
|
+
## Development
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
python3 -m unittest discover -s tests -t tests
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request.
|
|
113
|
+
|
|
114
|
+
The short version: standard library only, tests stay offline, and never let a coverage gap
|
|
115
|
+
report itself as a finding.
|
|
116
|
+
|
|
117
|
+
## Licence
|
|
118
|
+
|
|
119
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "touchneedle"
|
|
7
|
+
# Single source of truth: __version__ in scripts/touchneedle.py. setuptools
|
|
8
|
+
# parses it statically, without importing the module. See RELEASING.md.
|
|
9
|
+
dynamic = ["version"]
|
|
10
|
+
description = "Verify that every citation and reference in a document is real, accurately described, and consistently used."
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
license = "MIT"
|
|
14
|
+
license-files = ["LICENSE"]
|
|
15
|
+
authors = [{ name = "ncoleman" }]
|
|
16
|
+
keywords = ["citations", "references", "bibliography", "academic", "fact-checking"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Environment :: Console",
|
|
20
|
+
"Intended Audience :: Science/Research",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Topic :: Text Processing :: Markup",
|
|
28
|
+
"Topic :: Scientific/Engineering",
|
|
29
|
+
]
|
|
30
|
+
# Deliberately empty, and it should stay that way. The other install path is a
|
|
31
|
+
# bare `git clone` into a skills directory, where nothing gets pip-installed.
|
|
32
|
+
dependencies = []
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/nicoleman0/touchneedle"
|
|
36
|
+
Source = "https://github.com/nicoleman0/touchneedle"
|
|
37
|
+
Changelog = "https://github.com/nicoleman0/touchneedle/blob/main/CHANGELOG.md"
|
|
38
|
+
Issues = "https://github.com/nicoleman0/touchneedle/issues"
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
touchneedle = "touchneedle:main"
|
|
42
|
+
|
|
43
|
+
# One module, kept at scripts/touchneedle.py so that the path SKILL.md invokes
|
|
44
|
+
# is the same file pip installs. The two distribution paths never diverge.
|
|
45
|
+
[tool.setuptools]
|
|
46
|
+
py-modules = ["touchneedle"]
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.dynamic]
|
|
49
|
+
version = { attr = "touchneedle.__version__" }
|
|
50
|
+
|
|
51
|
+
[tool.setuptools.package-dir]
|
|
52
|
+
"" = "scripts"
|
|
53
|
+
|
|
54
|
+
# Dev-only, and deliberately a dependency group rather than an optional extra:
|
|
55
|
+
# a group can never be pulled in by `pip install touchneedle[...]`, so the
|
|
56
|
+
# zero-runtime-dependency promise above cannot be weakened by accident.
|
|
57
|
+
# Pinned with ~= because an unpinned linter adds rules on its own schedule and
|
|
58
|
+
# reddens CI on an unrelated pull request.
|
|
59
|
+
[dependency-groups]
|
|
60
|
+
dev = ["ruff~=0.16.5", "mypy~=2.3"]
|
|
61
|
+
|
|
62
|
+
[tool.ruff]
|
|
63
|
+
target-version = "py311"
|
|
64
|
+
line-length = 100
|
|
65
|
+
# plugins/ holds a generated copy of scripts/touchneedle.py -- linting it
|
|
66
|
+
# would report every finding twice. sync-plugin-skill.sh regenerates it.
|
|
67
|
+
extend-exclude = ["plugins/"]
|
|
68
|
+
|
|
69
|
+
[tool.ruff.lint]
|
|
70
|
+
select = ["E", "F", "W", "I", "UP", "B", "C4", "SIM"]
|
|
71
|
+
|
|
72
|
+
# `ruff format` is deliberately not run over this repo. build_report() and
|
|
73
|
+
# build_claims() assemble Markdown from hand-aligned list literals, which the
|
|
74
|
+
# formatter reflows into a diff nobody can review for no readability gain.
|
|
75
|
+
[tool.mypy]
|
|
76
|
+
python_version = "3.11"
|
|
77
|
+
strict = true
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: touchneedle
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Verify that every citation and reference in a document is real, accurately described, and consistently used.
|
|
5
|
+
Author: ncoleman
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nicoleman0/touchneedle
|
|
8
|
+
Project-URL: Source, https://github.com/nicoleman0/touchneedle
|
|
9
|
+
Project-URL: Changelog, https://github.com/nicoleman0/touchneedle/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/nicoleman0/touchneedle/issues
|
|
11
|
+
Keywords: citations,references,bibliography,academic,fact-checking
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Text Processing :: Markup
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# touchneedle
|
|
29
|
+
|
|
30
|
+
Citiation tool that verifies the citations in a document are real,
|
|
31
|
+
accurately described, and consistently used.
|
|
32
|
+
|
|
33
|
+
Useful for both students and examiners who wish to corroborate citations.
|
|
34
|
+
|
|
35
|
+
It works standalone, or as a coding agent skill.
|
|
36
|
+
|
|
37
|
+
Most commercial citation checkers want a `.bib` file and check it against academic
|
|
38
|
+
databases. That covers journal articles but misses standards,
|
|
39
|
+
specifications, vendor documentation, and blog posts. In a lot of real
|
|
40
|
+
bibliographies, this is half the list.
|
|
41
|
+
|
|
42
|
+
So this tool parses a **prose reference list** (Harvard/author-date)
|
|
43
|
+
straight out of Markdown or `.docx`, and routes each entry to whichever
|
|
44
|
+
authority can actually confirm it.
|
|
45
|
+
|
|
46
|
+
## What it checks
|
|
47
|
+
|
|
48
|
+
**Existence and metadata** — scripted and deterministic:
|
|
49
|
+
|
|
50
|
+
| Entry carries | Checked against |
|
|
51
|
+
|---|---|
|
|
52
|
+
| arXiv id | arXiv API |
|
|
53
|
+
| DOI | Crossref |
|
|
54
|
+
| RFC number | IETF datatracker, falling back to rfc-editor |
|
|
55
|
+
| `draft-*` name | IETF datatracker, **including whether the cited revision is still current** |
|
|
56
|
+
| Quoted title in an academic venue | Crossref, then OpenAlex, by title |
|
|
57
|
+
| A URL and nothing else | Fetched live; page title compared with the cited title |
|
|
58
|
+
|
|
59
|
+
Entries with both an identifier and a URL get both, so a real paper behind a dead
|
|
60
|
+
link is still reported. Detects the fabricated-citation signature — a real title
|
|
61
|
+
carrying the wrong authors — as `MISMATCH`.
|
|
62
|
+
|
|
63
|
+
**Internal consistency** — every in-text citation resolves to a list entry, every
|
|
64
|
+
list entry is cited somewhere, and `2025a`/`2025b` suffixes are used unambiguously.
|
|
65
|
+
|
|
66
|
+
**Claim support** — the pass that needs reading rather than fetching. `claims`
|
|
67
|
+
emits a worklist pairing each in-text citation with the sentence making the claim
|
|
68
|
+
and a locator for the source; the model then reads each source and rules
|
|
69
|
+
SUPPORTED / PARTIAL / UNSUPPORTED / INACCESSIBLE. This catches the failure the
|
|
70
|
+
database checks cannot: a genuine source attached to a claim it does not make.
|
|
71
|
+
|
|
72
|
+
## Install
|
|
73
|
+
|
|
74
|
+
As a command-line tool:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install touchneedle
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
As a Claude Code skill:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
git clone https://github.com/nicoleman0/touchneedle ~/.claude/skills/touchneedle
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Or as a Claude Code plugin:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
/plugin marketplace add nicoleman0/touchneedle
|
|
90
|
+
/plugin install touchneedle
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
There are no dependencies beyond Python 3.11+. `pandoc` is needed only for `.docx` input.
|
|
94
|
+
|
|
95
|
+
Then, in Claude Code: *"check the citations in thesis.docx"*.
|
|
96
|
+
|
|
97
|
+
## Use directly
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
touchneedle check thesis.docx --out report.md --json data.json
|
|
101
|
+
touchneedle claims thesis.docx --out claims.md
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
From a clone, without installing, that is `python3 scripts/touchneedle.py …` —
|
|
105
|
+
the same file either way.
|
|
106
|
+
|
|
107
|
+
Options: `--offline` (parse and cross-check only, no network), `--cache DIR`
|
|
108
|
+
(HTTP cache, 7-day TTL, so re-runs are nearly free), `--timeout N`, and
|
|
109
|
+
`--mailto you@example.com` for Crossref and OpenAlex's polite rate-limit pool.
|
|
110
|
+
`--mailto` is off by default and never inferred — it sends an address to third
|
|
111
|
+
parties.
|
|
112
|
+
|
|
113
|
+
`check` exits 2 when something needs attention, 0 when clean, so it drops into CI.
|
|
114
|
+
|
|
115
|
+
## Statuses
|
|
116
|
+
|
|
117
|
+
`MISMATCH` and `NOT_FOUND` are the ones that damage a submission. `LINK_DEAD` and
|
|
118
|
+
`STALE` need a fix but not a retraction. `PARTIAL`, `LINK_MOVED` and
|
|
119
|
+
`UNVERIFIABLE` are for a glance — notably, PDFs and JS-rendered pages land in
|
|
120
|
+
`PARTIAL` routinely, because no `<title>` can be read from them. A `PARTIAL` is a
|
|
121
|
+
limit of the check, not evidence against the citation.
|
|
122
|
+
|
|
123
|
+
## Limits
|
|
124
|
+
|
|
125
|
+
Author-date reference lists only — numeric styles (Vancouver, IEEE) are not
|
|
126
|
+
parsed. Page numbers, edition and publisher details are not checked.
|
|
127
|
+
|
|
128
|
+
Sources behind paywalls cannot be verified beyond their metadata record.
|
|
129
|
+
|
|
130
|
+
The list of in-text citations with no matching entry has expected false positives,
|
|
131
|
+
because a regex cannot distinguish `(Smith, 2024)` from `(ICLR 2023)`.
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
python3 -m unittest discover -s tests -t tests
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request.
|
|
140
|
+
|
|
141
|
+
The short version: standard library only, tests stay offline, and never let a coverage gap
|
|
142
|
+
report itself as a finding.
|
|
143
|
+
|
|
144
|
+
## Licence
|
|
145
|
+
|
|
146
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
scripts/touchneedle.py
|
|
5
|
+
scripts/touchneedle.egg-info/PKG-INFO
|
|
6
|
+
scripts/touchneedle.egg-info/SOURCES.txt
|
|
7
|
+
scripts/touchneedle.egg-info/dependency_links.txt
|
|
8
|
+
scripts/touchneedle.egg-info/entry_points.txt
|
|
9
|
+
scripts/touchneedle.egg-info/top_level.txt
|
|
10
|
+
tests/test_citations.py
|
|
11
|
+
tests/test_cli.py
|
|
12
|
+
tests/test_parsing.py
|
|
13
|
+
tests/test_verify.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
touchneedle
|