osaudit 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.
- osaudit-0.1.0/.gitignore +10 -0
- osaudit-0.1.0/CONVENTIONS.md +60 -0
- osaudit-0.1.0/LICENSE +21 -0
- osaudit-0.1.0/PKG-INFO +128 -0
- osaudit-0.1.0/README.md +103 -0
- osaudit-0.1.0/osaudit/__init__.py +3 -0
- osaudit-0.1.0/osaudit/__main__.py +7 -0
- osaudit-0.1.0/osaudit/_cli.py +114 -0
- osaudit-0.1.0/osaudit/_config.py +283 -0
- osaudit-0.1.0/osaudit/_findings.py +23 -0
- osaudit-0.1.0/osaudit/_git.py +45 -0
- osaudit-0.1.0/osaudit/_number.py +63 -0
- osaudit-0.1.0/osaudit/_numbers.py +122 -0
- osaudit-0.1.0/osaudit/_pdf.py +55 -0
- osaudit-0.1.0/osaudit/_provenance.py +150 -0
- osaudit-0.1.0/osaudit/_quotes.py +228 -0
- osaudit-0.1.0/osaudit/_results.py +124 -0
- osaudit-0.1.0/osaudit/_results_check.py +145 -0
- osaudit-0.1.0/osaudit/_tex.py +105 -0
- osaudit-0.1.0/pyproject.toml +53 -0
- osaudit-0.1.0/tests/fixtures/clean/audit.toml +22 -0
- osaudit-0.1.0/tests/fixtures/clean/paper/results.tex +17 -0
- osaudit-0.1.0/tests/fixtures/clean/results/panel.json +1 -0
- osaudit-0.1.0/tests/fixtures/clean/results/survey.json +2 -0
- osaudit-0.1.0/tests/fixtures/quotes/audit.toml +37 -0
- osaudit-0.1.0/tests/fixtures/quotes/paper.tex +7 -0
- osaudit-0.1.0/tests/fixtures/stale-age/audit.toml +18 -0
- osaudit-0.1.0/tests/fixtures/stale-age/paper/ages.tex +1 -0
- osaudit-0.1.0/tests/fixtures/stale-age/results/ages.json +2 -0
- osaudit-0.1.0/tests/test_extract.py +56 -0
- osaudit-0.1.0/tests/test_number.py +47 -0
- osaudit-0.1.0/tests/test_numbers.py +125 -0
- osaudit-0.1.0/tests/test_quotes.py +130 -0
- osaudit-0.1.0/tests/test_results.py +252 -0
osaudit-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Conventions of an automated open-science audit
|
|
2
|
+
|
|
3
|
+
A paper is auditable when every claim in it can be traced, mechanically, to the evidence that supports it.
|
|
4
|
+
This document states what a paper repository must contain, and in which format, so that `osaudit` can audit it.
|
|
5
|
+
Each rule is normative (MUST) and names the failure it prevents. Rule ids (`numbers/…`) are those printed by `osaudit`.
|
|
6
|
+
|
|
7
|
+
## Repository
|
|
8
|
+
|
|
9
|
+
- The repository root holds `audit.toml`, which declares where the paper, the results and the ledgers are. *Prevents: an audit that silently checks nothing because it looked in the wrong place.*
|
|
10
|
+
- The paper is LaTeX sources or a PDF. When both exist, the LaTeX sources are audited. *Prevents: auditing a stale PDF that no longer matches the sources.*
|
|
11
|
+
- Every machine-readable file of the audit (configuration, derivations, ledgers) is TOML. *Prevents: two parsers, two sets of quoting rules, two ways to be wrong.*
|
|
12
|
+
- An entry of `audit.toml` that matches nothing in the paper is a defect, and is reported. *Prevents: allow-lists and derivations that outlive the text they justified.*
|
|
13
|
+
|
|
14
|
+
## Numbers
|
|
15
|
+
|
|
16
|
+
- Every number printed in the paper is a results value, a declared derivation of results values, or an allow-list entry with a reason (`numbers/untraced`). *Prevents: an untraced number, such as a breakdown found in no results file, and different when recomputed.*
|
|
17
|
+
- Results are JSON files produced by a committed instrument; a value is named `<file stem>.<key>.<key>…`. *Prevents: numbers copied by hand from a terminal.*
|
|
18
|
+
- A printed number with d decimals matches a value v when it equals v, or 100·v for a percent, within half a unit of its last digit. *Prevents: false alarms on correct rounding (0.6745 printed 67.5%, or 67.4% when rounding half to even).*
|
|
19
|
+
- A number that is a sum, difference, product or share of results values is declared in `[[numbers.derive]]` with its expression (`value = "296"`, `expr = "survey.responses - survey.complete"`). *Prevents: derived numbers that no one can recompute, and a matcher that guesses derivations.*
|
|
20
|
+
- A declared derivation that evaluates to another value than the printed one is a defect (`numbers/derive-mismatch`). *Prevents: a number that drifted after the results were regenerated.*
|
|
21
|
+
- An expression that names a value absent from the results is a defect (`numbers/derive-unknown`). *Prevents: derivations over results that were renamed or removed.*
|
|
22
|
+
- A number that is not produced by the study (a year, a count published by a cited source, a parameter of the method) is allow-listed with `reason`, and with `cite` when it comes from a source (`[[numbers.allow]]`). *Prevents: an unexplained exemption.*
|
|
23
|
+
- A number taken from a cited source agrees with that source, verbatim. "Equals" is said only of equal numbers. *Prevents: a count claimed to equal a published count that differs from it in the fourth digit.*
|
|
24
|
+
- A number inside a verbatim quote is audited as part of the quote, not as a number. *Prevents: reporting a source's numbers as the study's.*
|
|
25
|
+
- Counts of parts add up to the whole that the text states, and each part is named ("8 runs: 6 pass, of which 1 with all tests skipped, 2 cannot start"). *Prevents: arithmetic that does not close.*
|
|
26
|
+
- The same quantity has the same value in every section; a different population is named and its difference explained ("1,204 of the 1,500 responses are complete, the 296 others are partial"). *Prevents: counts that silently differ across sections.*
|
|
27
|
+
|
|
28
|
+
## Results
|
|
29
|
+
|
|
30
|
+
- Every results file is a JSON object with a `provenance` block: `command` (argv), `cwd`, `inputs` (each `path` with its `sha256`), `commit` (git commit of the instruments), `now` (time anchor, ISO 8601 with time zone). `osaudit run` writes it (`results/no-provenance`). *Prevents: results that cannot be tied to the command and the data they claim to describe.*
|
|
31
|
+
- Results are produced from committed instruments: the instruments (`[results] instruments` in `audit.toml`) are unchanged since the recorded commit (`results/stale`). A results file that depends on only some of them declares them in `[[results.depends]]` (`file`, `instruments`). *Prevents: results of a version of the code that no longer exists, without rerunning every instrument for every change.*
|
|
32
|
+
- An input is a frozen snapshot: a directory input hashes as the manifest of its files, so a live directory (a log store that grows every day) never matches its record (`results/input-changed`, `results/input-missing`, with `--inputs`). *Prevents: results that no one can recompute, even with the same code.*
|
|
33
|
+
- Re-running the recorded command at the recorded time anchor prints the same results (`results/not-reproduced`, with `--rerun`). *Prevents: results that no instrument can produce any more, and numbers edited by hand.*
|
|
34
|
+
- An instrument never reads the clock or a file time: a time-dependent value (an age, a duration until now) is computed against the anchor in `$OSAUDIT_NOW`, recorded as `provenance.now` (`results/wall-clock`). A justified exception carries `osaudit: allow results/wall-clock -- <reason>` in a comment on the same line. *Prevents: values that shift between runs, such as ages computed against `now()` at analysis time, or against the modification time of a file.*
|
|
35
|
+
- A results file made by a person (a manual check of an automated classifier) is declared in `[[results.manual]]` with `file`, `by`, `date`, `reason`, and is exempt from provenance. *Prevents: a human judgment passed off as an instrument output, or rejected for not being one.*
|
|
36
|
+
- An exemption or a dependency entry that applies to nothing, inline or in `audit.toml`, is a defect (`results/unused`). *Prevents: exemptions that outlive the code they justified.*
|
|
37
|
+
- Every instrument is tested on a known-positive, a known-negative and a boundary case. *Prevents: trusting the numbers of an unvalidated instrument.*
|
|
38
|
+
|
|
39
|
+
## Quotes
|
|
40
|
+
|
|
41
|
+
- Every verbatim quote with a citation in the same sentence has an entry in the quote ledger, the `[[quotes]]` entries of `audit.toml` (`quotes/no-entry`). *Prevents: a quote no one checked.*
|
|
42
|
+
- A ledger entry records the citation `key`, the full sentence `text` verbatim, the `version` read (`publisher`, `author-pdf`, `preprint`, `reprint`, `abstract`, `secondary`), its `url`, the `page`, the `date` read and the `status` (`verified` or `unverified`), optionally the `claim` it supports. *Prevents: losing which text was actually read.*
|
|
43
|
+
- The quote of the paper is verbatim in the entry's text; `[...]` marks a cut (`quotes/mismatch`). *Prevents: a quote reworded, or attributed to a text that does not contain it.*
|
|
44
|
+
- The page cited is the page recorded (`quotes/page`), and an entry read in a paged version records its page (`quotes/no-page`). *Prevents: a page no one read.*
|
|
45
|
+
- Only a `verified` entry is quoted (`quotes/unverified`); an entry that matches no quote is removed (`quotes/unused`). *Prevents: quoting before checking, and a ledger that drifts from the paper.*
|
|
46
|
+
- A quote is `verified` only when its wording was matched against the version read; a summary by a search engine or a model is never a version read. *Prevents: a sentence that summaries attribute to an author, but that is not in the text.*
|
|
47
|
+
- The version read is the version cited; when they differ, the numbers and pages are those of the version cited, or the citation changes. *Prevents: citing a published version with the numbers of its preprint, which differ.*
|
|
48
|
+
- A quote from an abstract has no page and the ledger says `abstract`; a page read from a reprint is not the page of the original. *Prevents: invented page numbers.*
|
|
49
|
+
- A shortened quote keeps the scope of the full sentence; the ledger holds the full sentence. *Prevents: a truncated quote that widens the author's claim, such as a quote that drops "some of".*
|
|
50
|
+
|
|
51
|
+
## References
|
|
52
|
+
|
|
53
|
+
- A source is cited for a position it holds, not for one it refutes or does not address; the ledger entry records the claim the citation supports. *Prevents: citing a paper for the position it refutes, or a study of one measure for another measure.*
|
|
54
|
+
- Every cited BibTeX entry has a verification record (date, what was checked, against which source) and a DOI or a URL. *Prevents: references with invented fields.*
|
|
55
|
+
- Every cross-reference resolves, and the text names what it points to. *Prevents: text pointing to an example removed in an earlier revision.*
|
|
56
|
+
|
|
57
|
+
## Reviews
|
|
58
|
+
|
|
59
|
+
- Every finding of an automated reviewer is a claim, verified by a tool before it is acted on. *Prevents: changing the author's prose for a style defect that the reviewer reported and the prose checker does not confirm.*
|
|
60
|
+
- A verified finding is reported with the tool output that confirms it; an unconfirmed finding is reported as unconfirmed, never acted on. *Prevents: reviewer hallucinations entering the paper as corrections.*
|
osaudit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Martin Monperrus
|
|
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.
|
osaudit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: osaudit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Automated open-science audit: trace every claim of a paper to its evidence
|
|
5
|
+
Project-URL: Homepage, https://github.com/ASSERT-KTH/osaudit
|
|
6
|
+
Project-URL: Issues, https://github.com/ASSERT-KTH/osaudit/issues
|
|
7
|
+
Author: Martin Monperrus
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: audit,citations,latex,open science,provenance,reproducibility
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering
|
|
17
|
+
Classifier: Topic :: Text Processing :: Markup :: LaTeX
|
|
18
|
+
Requires-Python: >=3.13
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
23
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# osaudit
|
|
27
|
+
|
|
28
|
+
Automated open-science audit: report every claim of a scientific paper that is not traced, mechanically, to the evidence that supports it. A number to a results file, a quote to a page of a verified source, a result to the instrument and the input that produced it. Silent when everything is traced.
|
|
29
|
+
|
|
30
|
+
What a paper repository must contain to be auditable is in [CONVENTIONS.md](CONVENTIONS.md).
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
uv venv && uv pip install -e '.[dev]'
|
|
35
|
+
|
|
36
|
+
PDF input needs `pdftotext` (poppler-utils).
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
In the paper repository, write `audit.toml`:
|
|
41
|
+
|
|
42
|
+
```toml
|
|
43
|
+
[paper]
|
|
44
|
+
tex = ["paper/sections/*.tex"] # or: pdf = "paper/main.pdf"
|
|
45
|
+
|
|
46
|
+
[results] # optional: a paper without a study audits its quotes only
|
|
47
|
+
files = ["study/results/*.json"]
|
|
48
|
+
instruments = ["study/*.py"] # the code that produces the results
|
|
49
|
+
|
|
50
|
+
[[results.depends]] # optional: what one results file depends on (default: all)
|
|
51
|
+
file = "study/results/survey.json"
|
|
52
|
+
instruments = ["study/survey.py", "study/_common.py"]
|
|
53
|
+
|
|
54
|
+
[[results.manual]] # a results file made by a person
|
|
55
|
+
file = "study/results/labels_check.json"
|
|
56
|
+
by = "A. Author"
|
|
57
|
+
date = "2026-10-17"
|
|
58
|
+
reason = "the author checked 20 labels of the classifier"
|
|
59
|
+
|
|
60
|
+
[[numbers.derive]] # a number computed from results values
|
|
61
|
+
value = "392"
|
|
62
|
+
expr = "panel.overall.total - panel.overall.answers.yes"
|
|
63
|
+
|
|
64
|
+
[[numbers.allow]] # a number the study does not produce
|
|
65
|
+
value = "2,000,000"
|
|
66
|
+
reason = "population size published by the cited census"
|
|
67
|
+
cite = "census2020"
|
|
68
|
+
|
|
69
|
+
[[quotes]] # a quote as read in its source
|
|
70
|
+
key = "doe2001archive" # BibTeX key
|
|
71
|
+
text = "We call the archive the second memory of a laboratory, after the minds of its members."
|
|
72
|
+
version = "publisher" # publisher|author-pdf|preprint|reprint|abstract|secondary
|
|
73
|
+
url = "https://doi.org/10.1000/doe2001"
|
|
74
|
+
page = "12" # omitted for an abstract
|
|
75
|
+
date = "2026-10-01" # read on
|
|
76
|
+
status = "verified" # verified|unverified
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Then:
|
|
80
|
+
|
|
81
|
+
osaudit # every check (static parts)
|
|
82
|
+
osaudit numbers # every printed number traced to results, derivation or allow
|
|
83
|
+
osaudit quotes # every cited quote verbatim in a verified [[quotes]] entry
|
|
84
|
+
osaudit results # every results file has a provenance that still holds
|
|
85
|
+
osaudit results --inputs # ... and its inputs still have their recorded sha256
|
|
86
|
+
osaudit results --rerun # ... and its command, rerun at its time anchor, prints the same
|
|
87
|
+
osaudit CHECK --json # a JSON array of findings, [] when clean
|
|
88
|
+
osaudit CHECK --root PATH --config FILE
|
|
89
|
+
|
|
90
|
+
One line per finding, `file:line: rule: message` (for a PDF, the line is the page):
|
|
91
|
+
|
|
92
|
+
paper/results.tex:6: numbers/untraced: 392 matches no results value, derivation or allow entry; candidate expr: panel.overall.total - panel.overall.answers.yes | survey.complete - panel.overall.answers.yes
|
|
93
|
+
audit.toml:8: numbers/derive-mismatch: 70.0% declared, but ages.by_age["<30"].holds / ages.by_age["<30"].informative = 0.625, printed 62.5%
|
|
94
|
+
paper.tex:2: quotes/mismatch: ``the only memory of a laboratory'' is not verbatim in the entry of doe2001archive (closest: audit.toml:4)
|
|
95
|
+
study/results/survey.json:1: results/no-provenance: no 'provenance' block
|
|
96
|
+
study/ages.py:12: results/wall-clock: datetime.now( reads the clock or a file time; read the time anchor from $OSAUDIT_NOW instead
|
|
97
|
+
|
|
98
|
+
Exit status: 0 clean, 1 findings, 2 the audit could not run (configuration, input).
|
|
99
|
+
|
|
100
|
+
### Producing results
|
|
101
|
+
|
|
102
|
+
Produce each results file through `osaudit run`, which records its provenance (command, inputs and their sha256, git commit, time anchor) in the file:
|
|
103
|
+
|
|
104
|
+
osaudit run --input data/answers.csv -o study/results/survey.json -- python study/survey.py data/answers.csv
|
|
105
|
+
|
|
106
|
+
The instrument prints its results as a JSON object on stdout, and reads the time anchor from `$OSAUDIT_NOW` instead of the clock. Its instruments must be committed; `run` refuses otherwise. A clock read that is justified carries `# osaudit: allow results/wall-clock -- <reason>` on its line.
|
|
107
|
+
|
|
108
|
+
### Results values
|
|
109
|
+
|
|
110
|
+
The `provenance` block is not a result and is never matched. A value is named `<file stem>.<key>...`, with `["key"]` for keys that are not identifiers and `[i]` for list indices: `ages.by_age["<30"].share`. A printed number matches a value within half a unit of its last digit; a percent matches 100·v. Expressions allow `+ - * /`, parentheses and numeric constants, and are never evaluated as Python.
|
|
111
|
+
|
|
112
|
+
Candidates proposed for an untraced number are single operations over counts (sums, differences, shares), ranked by closeness in the results tree. They are hints: nothing is accepted until written into `audit.toml`.
|
|
113
|
+
|
|
114
|
+
### What is not audited as a number
|
|
115
|
+
|
|
116
|
+
LaTeX comments, `tikzpicture` and verbatim environments, verbatim quotes (` ``...'' `, audited as quotes), arguments of `\input \label \cref \cite \url \begin` and similar, parts of a document (`Section 4.3`, `Table~1`, `p.~107`), and numbers glued to a word (`123d0d04`, `3.0cm`, `RQ1`). In a PDF: numbered citations, list labels, lines holding only a number, and everything after the last "References" heading.
|
|
117
|
+
|
|
118
|
+
### Quotes
|
|
119
|
+
|
|
120
|
+
A quote is ` ``...'' ` with a citation in the same sentence (the nearest after it, else before it); its page is the `p.~X` of that citation. It must be verbatim, after normalization of LaTeX, typography and case, in the full sentence of an entry for that key; `[...]` marks a cut. In a PDF, citations are numbered, so a quote `“...”` matches any entry by its text.
|
|
121
|
+
|
|
122
|
+
## Status
|
|
123
|
+
|
|
124
|
+
`numbers`, `quotes`, `results` and `run` are implemented. `refs` and `review` are specified in CONVENTIONS.md and not yet built; they will be subcommands of `osaudit` too.
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT
|
osaudit-0.1.0/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# osaudit
|
|
2
|
+
|
|
3
|
+
Automated open-science audit: report every claim of a scientific paper that is not traced, mechanically, to the evidence that supports it. A number to a results file, a quote to a page of a verified source, a result to the instrument and the input that produced it. Silent when everything is traced.
|
|
4
|
+
|
|
5
|
+
What a paper repository must contain to be auditable is in [CONVENTIONS.md](CONVENTIONS.md).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
uv venv && uv pip install -e '.[dev]'
|
|
10
|
+
|
|
11
|
+
PDF input needs `pdftotext` (poppler-utils).
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
In the paper repository, write `audit.toml`:
|
|
16
|
+
|
|
17
|
+
```toml
|
|
18
|
+
[paper]
|
|
19
|
+
tex = ["paper/sections/*.tex"] # or: pdf = "paper/main.pdf"
|
|
20
|
+
|
|
21
|
+
[results] # optional: a paper without a study audits its quotes only
|
|
22
|
+
files = ["study/results/*.json"]
|
|
23
|
+
instruments = ["study/*.py"] # the code that produces the results
|
|
24
|
+
|
|
25
|
+
[[results.depends]] # optional: what one results file depends on (default: all)
|
|
26
|
+
file = "study/results/survey.json"
|
|
27
|
+
instruments = ["study/survey.py", "study/_common.py"]
|
|
28
|
+
|
|
29
|
+
[[results.manual]] # a results file made by a person
|
|
30
|
+
file = "study/results/labels_check.json"
|
|
31
|
+
by = "A. Author"
|
|
32
|
+
date = "2026-10-17"
|
|
33
|
+
reason = "the author checked 20 labels of the classifier"
|
|
34
|
+
|
|
35
|
+
[[numbers.derive]] # a number computed from results values
|
|
36
|
+
value = "392"
|
|
37
|
+
expr = "panel.overall.total - panel.overall.answers.yes"
|
|
38
|
+
|
|
39
|
+
[[numbers.allow]] # a number the study does not produce
|
|
40
|
+
value = "2,000,000"
|
|
41
|
+
reason = "population size published by the cited census"
|
|
42
|
+
cite = "census2020"
|
|
43
|
+
|
|
44
|
+
[[quotes]] # a quote as read in its source
|
|
45
|
+
key = "doe2001archive" # BibTeX key
|
|
46
|
+
text = "We call the archive the second memory of a laboratory, after the minds of its members."
|
|
47
|
+
version = "publisher" # publisher|author-pdf|preprint|reprint|abstract|secondary
|
|
48
|
+
url = "https://doi.org/10.1000/doe2001"
|
|
49
|
+
page = "12" # omitted for an abstract
|
|
50
|
+
date = "2026-10-01" # read on
|
|
51
|
+
status = "verified" # verified|unverified
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Then:
|
|
55
|
+
|
|
56
|
+
osaudit # every check (static parts)
|
|
57
|
+
osaudit numbers # every printed number traced to results, derivation or allow
|
|
58
|
+
osaudit quotes # every cited quote verbatim in a verified [[quotes]] entry
|
|
59
|
+
osaudit results # every results file has a provenance that still holds
|
|
60
|
+
osaudit results --inputs # ... and its inputs still have their recorded sha256
|
|
61
|
+
osaudit results --rerun # ... and its command, rerun at its time anchor, prints the same
|
|
62
|
+
osaudit CHECK --json # a JSON array of findings, [] when clean
|
|
63
|
+
osaudit CHECK --root PATH --config FILE
|
|
64
|
+
|
|
65
|
+
One line per finding, `file:line: rule: message` (for a PDF, the line is the page):
|
|
66
|
+
|
|
67
|
+
paper/results.tex:6: numbers/untraced: 392 matches no results value, derivation or allow entry; candidate expr: panel.overall.total - panel.overall.answers.yes | survey.complete - panel.overall.answers.yes
|
|
68
|
+
audit.toml:8: numbers/derive-mismatch: 70.0% declared, but ages.by_age["<30"].holds / ages.by_age["<30"].informative = 0.625, printed 62.5%
|
|
69
|
+
paper.tex:2: quotes/mismatch: ``the only memory of a laboratory'' is not verbatim in the entry of doe2001archive (closest: audit.toml:4)
|
|
70
|
+
study/results/survey.json:1: results/no-provenance: no 'provenance' block
|
|
71
|
+
study/ages.py:12: results/wall-clock: datetime.now( reads the clock or a file time; read the time anchor from $OSAUDIT_NOW instead
|
|
72
|
+
|
|
73
|
+
Exit status: 0 clean, 1 findings, 2 the audit could not run (configuration, input).
|
|
74
|
+
|
|
75
|
+
### Producing results
|
|
76
|
+
|
|
77
|
+
Produce each results file through `osaudit run`, which records its provenance (command, inputs and their sha256, git commit, time anchor) in the file:
|
|
78
|
+
|
|
79
|
+
osaudit run --input data/answers.csv -o study/results/survey.json -- python study/survey.py data/answers.csv
|
|
80
|
+
|
|
81
|
+
The instrument prints its results as a JSON object on stdout, and reads the time anchor from `$OSAUDIT_NOW` instead of the clock. Its instruments must be committed; `run` refuses otherwise. A clock read that is justified carries `# osaudit: allow results/wall-clock -- <reason>` on its line.
|
|
82
|
+
|
|
83
|
+
### Results values
|
|
84
|
+
|
|
85
|
+
The `provenance` block is not a result and is never matched. A value is named `<file stem>.<key>...`, with `["key"]` for keys that are not identifiers and `[i]` for list indices: `ages.by_age["<30"].share`. A printed number matches a value within half a unit of its last digit; a percent matches 100·v. Expressions allow `+ - * /`, parentheses and numeric constants, and are never evaluated as Python.
|
|
86
|
+
|
|
87
|
+
Candidates proposed for an untraced number are single operations over counts (sums, differences, shares), ranked by closeness in the results tree. They are hints: nothing is accepted until written into `audit.toml`.
|
|
88
|
+
|
|
89
|
+
### What is not audited as a number
|
|
90
|
+
|
|
91
|
+
LaTeX comments, `tikzpicture` and verbatim environments, verbatim quotes (` ``...'' `, audited as quotes), arguments of `\input \label \cref \cite \url \begin` and similar, parts of a document (`Section 4.3`, `Table~1`, `p.~107`), and numbers glued to a word (`123d0d04`, `3.0cm`, `RQ1`). In a PDF: numbered citations, list labels, lines holding only a number, and everything after the last "References" heading.
|
|
92
|
+
|
|
93
|
+
### Quotes
|
|
94
|
+
|
|
95
|
+
A quote is ` ``...'' ` with a citation in the same sentence (the nearest after it, else before it); its page is the `p.~X` of that citation. It must be verbatim, after normalization of LaTeX, typography and case, in the full sentence of an entry for that key; `[...]` marks a cut. In a PDF, citations are numbered, so a quote `“...”` matches any entry by its text.
|
|
96
|
+
|
|
97
|
+
## Status
|
|
98
|
+
|
|
99
|
+
`numbers`, `quotes`, `results` and `run` are implemented. `refs` and `review` are specified in CONVENTIONS.md and not yet built; they will be subcommands of `osaudit` too.
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""The `osaudit` command line: silent when the paper is clean, one line per finding otherwise.
|
|
2
|
+
|
|
3
|
+
Exit status: 0 no finding, 1 findings, 2 the audit could not run (configuration, input).
|
|
4
|
+
`osaudit run` writes a results file with its provenance; it prints nothing on success.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from collections.abc import Callable, Sequence
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from . import __version__, _numbers, _provenance, _quotes, _results_check
|
|
16
|
+
from ._config import Config, ConfigError, instruments, load
|
|
17
|
+
from ._findings import Finding
|
|
18
|
+
from ._git import GitError
|
|
19
|
+
from ._pdf import PdfError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _results(cfg: Config, args: argparse.Namespace) -> list[Finding]:
|
|
23
|
+
return _results_check.check(cfg, inputs=getattr(args, "inputs", False),
|
|
24
|
+
rerun=getattr(args, "rerun", False),
|
|
25
|
+
timeout=getattr(args, "timeout", None))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
CHECKS: dict[str, tuple[Callable[[Config, argparse.Namespace], list[Finding]], str]] = {
|
|
29
|
+
"numbers": (lambda cfg, _: _numbers.check(cfg),
|
|
30
|
+
"every number of the paper traced to results, derivation or allow"),
|
|
31
|
+
"quotes": (lambda cfg, _: _quotes.check(cfg),
|
|
32
|
+
"every cited verbatim quote in a verified [[quotes]] entry, with its page"),
|
|
33
|
+
"results": (_results,"every results file has a provenance that still holds "
|
|
34
|
+
"(static; --inputs, --rerun to go further)"),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parser() -> argparse.ArgumentParser:
|
|
39
|
+
p = argparse.ArgumentParser(
|
|
40
|
+
prog="osaudit",
|
|
41
|
+
description="Audit a paper repository: report every claim not traced to its evidence. "
|
|
42
|
+
"Silent when clean; `file:line: rule: message` per finding. "
|
|
43
|
+
"Conventions: CONVENTIONS.md. Without a check name, runs every check "
|
|
44
|
+
"(static parts only).",
|
|
45
|
+
epilog="exit status: 0 clean, 1 findings, 2 the audit could not run",
|
|
46
|
+
)
|
|
47
|
+
p.add_argument("--version", action="version", version=f"osaudit {__version__}")
|
|
48
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
49
|
+
common.add_argument("--root", type=Path, default=Path("."),
|
|
50
|
+
help="paper repository (default: current directory)")
|
|
51
|
+
common.add_argument("--config", type=Path, help="audit.toml (default: <root>/audit.toml)")
|
|
52
|
+
common.add_argument("--json", action="store_true",
|
|
53
|
+
help="print findings as a JSON array ([] when clean)")
|
|
54
|
+
sub = p.add_subparsers(dest="check", metavar="CHECK")
|
|
55
|
+
for name, (_, help_) in CHECKS.items():
|
|
56
|
+
sp = sub.add_parser(name, parents=[common], help=help_, description=help_)
|
|
57
|
+
if name == "results":
|
|
58
|
+
sp.add_argument("--inputs", action="store_true",
|
|
59
|
+
help="rehash every recorded input (needs the inputs)")
|
|
60
|
+
sp.add_argument("--rerun", action="store_true",
|
|
61
|
+
help="rerun every recorded command at its time anchor and compare")
|
|
62
|
+
sp.add_argument("--timeout", type=float, help="seconds per rerun")
|
|
63
|
+
sub.add_parser("all", parents=[common], help="run every check (the default)")
|
|
64
|
+
run = sub.add_parser(
|
|
65
|
+
"run", help="run an instrument, write its JSON results with their provenance",
|
|
66
|
+
description="Run CMD (its stdout must be a JSON object) with the time anchor in "
|
|
67
|
+
f"${_provenance.ENV_NOW}, and write the object to OUTPUT with a provenance "
|
|
68
|
+
"block: command, inputs and their sha256, git commit, time anchor. "
|
|
69
|
+
"Refuses to run on a tree with uncommitted changes.",
|
|
70
|
+
epilog="example: osaudit run --input data/answers.csv -o results/survey.json -- "
|
|
71
|
+
"python study/survey.py data/answers.csv")
|
|
72
|
+
run.add_argument("--root", type=Path, default=Path("."), help="repository of the instruments")
|
|
73
|
+
run.add_argument("--input", type=Path, action="append", default=[], dest="input_paths",
|
|
74
|
+
help="an input file or directory (repeat for each)")
|
|
75
|
+
run.add_argument("-o", "--output", type=Path, required=True, help="results file to write")
|
|
76
|
+
run.add_argument("cmd", nargs=argparse.REMAINDER, help="-- CMD [ARG ...]")
|
|
77
|
+
return p
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _run(args: argparse.Namespace) -> int:
|
|
81
|
+
cmd = args.cmd[1:] if args.cmd[:1] == ["--"] else args.cmd
|
|
82
|
+
if not cmd:
|
|
83
|
+
print("osaudit run: no command (put it after --)", file=sys.stderr)
|
|
84
|
+
return 2
|
|
85
|
+
root = args.root.resolve()
|
|
86
|
+
try:
|
|
87
|
+
output = args.output.resolve()
|
|
88
|
+
out_rel = str(output.relative_to(root)) if output.is_relative_to(root) else str(output)
|
|
89
|
+
_provenance.run(root, cmd, args.input_paths, args.output, instruments(root, out_rel))
|
|
90
|
+
except (_provenance.RunError, ConfigError, GitError, OSError) as e:
|
|
91
|
+
print(f"osaudit run: {e}", file=sys.stderr)
|
|
92
|
+
return 2
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
97
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
98
|
+
if not (argv and argv[0] in (*CHECKS, "all", "run", "-h", "--help", "--version")):
|
|
99
|
+
argv = ["all", *argv]
|
|
100
|
+
args = parser().parse_args(argv)
|
|
101
|
+
if args.check == "run":
|
|
102
|
+
return _run(args)
|
|
103
|
+
names = list(CHECKS) if args.check == "all" else [args.check]
|
|
104
|
+
try:
|
|
105
|
+
cfg = load(args.root.resolve(), args.config.resolve() if args.config else None)
|
|
106
|
+
findings = [f for n in names for f in CHECKS[n][0](cfg, args)]
|
|
107
|
+
except (ConfigError, PdfError, GitError, OSError, ValueError) as e:
|
|
108
|
+
print(f"osaudit: {e}", file=sys.stderr)
|
|
109
|
+
return 2
|
|
110
|
+
if args.json:
|
|
111
|
+
print(json.dumps([f.as_json() for f in findings], indent=1))
|
|
112
|
+
else:
|
|
113
|
+
sys.stdout.write("".join(f"{f}\n" for f in findings))
|
|
114
|
+
return 1 if findings else 0
|