This directory is a deployable Python package named tmaudit. It
ships three things in one place:
- The templates for the three operational scripts that every
PAPER<N>_CONSOLIDATED/directory needs (verify_p<N>.py,_compile_check.py,_fix_abstract_unicode.py). - The per-paper configurations (C1..C10 audit rules: C1–C7 in v0.1.0–v0.2.0, plus C8 statistical power, C9 figure-caption, and C10 reproducibility in v0.3.0; abstract Unicode replacements) for Paper 1, 2, 3, 4, 5.
- A CLI (
tmaudit) that forks the templates and (optionally) runs them.
Scope, stated plainly: the built-in rulepack covers this
project's five papers. verify / compile / fix-unicode /
audit-all take --paper 1..5, and --paper-dir changes where the
forked script is written — not which rules it contains. For a manuscript
those configs were not written for, tmaudit verify --paper-dir DIR
(no --paper) forks the factory template into DIR: the per-paper
categories report not applicable and the generic categories run. To add
checks the generic categories do not have, write your own plugin: see
docs/PLUGINS.md.
# on any machine with Python 3.9+
python tmaudit.pyz list
python tmaudit.pyz verify --paper 1
python tmaudit.pyz compile --paper 5
python tmaudit.pyz fix-unicode --paper 1
python tmaudit.pyz audit-allThe .pyz is a self-extracting Python zip archive; the user does not
need to pip install anything. The file is ~64 KB and contains all
five PAPER_CONFIGS and all three templates.
It is a convenient way to hand the tool to a collaborator working in
the same research tree: the built-in configs point at
PAPER1..PAPER5_CONSOLIDATED, so they will need
TMAUDIT_RESEARCH_DIR set to a tree with that layout. For a manuscript
outside those five papers the built-in commands will not audit it —
write a plugin instead (docs/PLUGINS.md).
cd <your tmaudit checkout>
python -m pip install --editable .
python -m pip install --editable ./tmaudit_example_plugin
tmaudit list
tmaudit verify --paper 1The second install is not optional for tests: tests/test_example_plugin.py
asserts that the in-repo demo plugin is discoverable via registered
entry points. Without that editable install, 11 plugin tests fail with
KeyError: 'flag-long-abstract'. The CI workflow (ci.yml) runs the
same two installs.
Edits to src/tmaudit/templates/*.py or
src/tmaudit/configs/*.py are picked up immediately on the next
tmaudit invocation. This is the recommended form for ongoing
development of the templates themselves.
python -m venv .venv
source .venv/bin/activate # or .venv\Scripts\activate on Windows
pip install .
tmaudit listThis produces a tmaudit console-script entry in the venv. The
template and config files are inside the wheel.
pip install tmaudit # or: pipx install tmaudit
tmaudit listPublished on PyPI via trusted publishing (OIDC) from
.github/workflows/publish-pypi.yml; new versions are cut from GitHub
Releases. This is the quickest form for someone who just wants the CLI.
The built-in categories are this project's rulepack for five papers. To
add checks of your own — including for a manuscript those configs do not
cover — write a plugin: a function decorated with @check, registered
under the tmaudit.plugins entry-point group, receiving your paper's
root .tex and returning Finding objects. (The generic categories run
without a plugin via tmaudit verify --paper-dir /path/to/your/paper.)
tmaudit verify --paper-dir /path/to/your/paper # generic checks, no config
pip install -e ./my-paper-checks # plugins are found via installed entry points
tmaudit plugins list # confirm it registered
tmaudit plugins run my-check --paper-dir /path/to/your/paperThe full contract, a copy-paste package layout, and what a plugin does
and does not get are in docs/PLUGINS.md. A working
three-check example ships in
tmaudit_example_plugin/.
The rulepack is per-paper by design, so the sharpest question a reader can
ask is: what does this tool do on a paper its config was not written for?
We ran all ten categories over 25 real arXiv cs.CL LaTeX sources,
exactly as main() runs them.
It found four defects in the tool itself:
- the factory
CHECKS_CONFIGwas one specific paper's config, so a generic run fired that paper's checks against any document; - "clean" could mean "did not run" — a category with no configuration reported zero findings, indistinguishable from a pass;
- one missing config key aborted the whole audit with a
KeyError; - the BibTeX key pattern fabricated "missing citation" findings for keys
that were present in
refs.bib, whenever a key sat on its own line.
All four are fixed in v0.5.1, with regression tests; the two smaller
findings (a SyntaxWarning on import, and an observation failure filed as a
HIGH finding) are fixed as well. The v0.5.0 findings are kept verbatim as
evidence, and the harness restores the v0.5.0 config so the config-driven
defects replay: case-studies/arxiv-corpus/
has the coverage matrix, the method, and what the numbers do not
establish.
case-studies/arxiv-2502.14074/
runs the two public extension paths — verify --paper-dir and two plugin
checks — against one real paper (Investigating Non-Transitivity in
LLM-as-a-Judge, arXiv:2502.14074, ICML 2025). It reports two
reporting-standards observations, records three candidate weaknesses that
were checked and withdrawn, and surfaced a caption-parsing defect in
the tool (nested braces), fixed in this commit. Every input is pinned by
SHA-256; the results, and what the audit does not establish, are in its
README.
Released: 2026-07-10. This release adds 3 new audit categories (C8, C9, C10) and brings the project to 10 categories, 176 unit tests, 12/12 meta-test bugs caught, 5/5 papers auditable.
| Category | What it checks | Severity | Always runs? |
|---|---|---|---|
| C1 (v0.1.0) | Symbol inline definition | HIGH | Yes |
| C2 (v0.1.0) | Bonferroni consistency | HIGH | Yes |
| C3 (v0.1.0) | Formalization of key concept | MEDIUM | Yes |
| C4 (v0.1.0) | Self-citation threshold | MEDIUM | Yes |
| C5 (v0.1.0) | Test-name d | MEDIUM | Yes |
| C6 (v0.1.1) | Blacklist word frequency | LOW | Yes |
| C7 (v0.1.2) | Ceremonial citation detection | MEDIUM | Yes |
| C8 (v0.3.0) | Statistical power (d-mismatch, post-hoc power) | HIGH/MED | No (opt-in) |
| C9 (v0.3.0) | Figure-caption consistency (4 sub-checks) | HIGH/MED | Yes (sub-check 3 opt-in) |
| C10 (v0.3.0) | Reproducibility (availability, metadata, consistency) | HIGH/MED/LOW | Yes (sub-check 2 opt-in) |
| C7-LLM (v0.2.0) | LLM fallback for C7 borderline cases (opt-in via TMAUDIT_LLM_* env) |
MEDIUM | No |
v0.2.0 added (still shipping): audit-all --output report.md
generates a single Markdown report covering all papers; the report
is uploaded as a CI artefact.
v0.2.0 added (still shipping): per-paper config for Paper 2, Paper 3, Paper 4 (was placeholder in v0.1.1). All 5 papers (P1..P5) are now auditable.
Per-paper config for v0.3.0: c8_claimed_effects (statistical),
c9_figure_keywords (figure-caption), c10_reproducibility_claims
(reproducibility). Empty list = opt-out (with graceful degradation
in C9 and C10).
See RELEASE_NOTES_v0.3.0.md for the
full release notes and engineering_notes_verify_template.md
§12 (C8+C10) and §13 (C9) for the design rationale.
tmaudit list # show all known paper configs
tmaudit verify --paper N [--dry-run] # fork verify_pN.py + run audit
tmaudit compile --paper N [--dry-run] # fork _compile_check.py + 4-pass build
tmaudit fix-unicode --paper N [--dry-run] # fork _fix_abstract_unicode.py
tmaudit audit-all [--start 1] [--end 5] # verify all papers in a range
Every subcommand also accepts --paper-dir PATH to override the
default paper directory.
cd F:\Research\PAPER<N>_CONSOLIDATED
# 1. audit
tmaudit verify --paper N
# 2. fix abstract Unicode (if any finding)
tmaudit fix-unicode --paper N
tmaudit verify --paper N
# 3. compile
tmaudit compile --paper NTEMPLATE/
├── pyproject.toml # build config
├── tmaudit.pyz # single-file distribution (64 KB)
├── README.md # this file
├── engineering_notes_verify_template.md # 6-bug engineering diary
├── _setup_pkg.py # one-time: copy templates into src/
├── _build_pyz.py # rebuild tmaudit.pyz after any src/ change
├── _compile_appendix.py # LaTeX compile for the appendix
├── _regen_appendix.py # regenerate appendix_engineering.tex
├── _status.py # TEMPLATE/ state summary
└── src/
└── tmaudit/ # the actual package
├── __init__.py
├── __main__.py # `python -m tmaudit`
├── cli.py # argparse CLI
├── forge.py # template-substitution engine
├── templates/
│ ├── __init__.py
│ ├── verify_TEMPLATE.py # ≈ 22 KB, ~400 lines
│ ├── compile_check_TEMPLATE.py # ≈ 4.5 KB
│ └── fix_abstract_unicode_TEMPLATE.py # ≈ 5 KB
└── configs/
├── __init__.py # re-exports
├── paper_configs.py # VERIFY_CONFIGS for paper 1..5
└── compile_configs.py # COMPILE_CONFIGS for paper 1..5
Edit two files:
src/tmaudit/configs/paper_configs.py— add a new entry toPAPER_CONFIGS(orVERIFY_CONFIGSafter re-export). The fields are documented in the docstring at the top of that file.src/tmaudit/configs/compile_configs.py— add a matching entry toCOMPILE_CONFIGS. The fields are documented in the docstring there.
Then:
python -m pip install --editable . --break-system-packages
python _build_pyz.py
tmaudit list # confirm Paper 6 shows up
tmaudit verify --paper 6 # fork + run; will report any HIGH-severity
# issues caused by your config choices# On the source machine:
python _build_pyz.py
# -> tmaudit.pyz (64 KB)
# Copy tmaudit.pyz to a fresh machine with no Python packages installed
# (only a base Python 3.9+ interpreter):
scp tmaudit.pyz other-host:/tmp/
ssh other-host 'python /tmp/tmaudit.pyz list'
# -> should print the same 5-paper tableThe .pyz has no external dependencies (only the Python standard
library). It works on Linux, macOS, and Windows alike.
The twelve historical bugs encountered while building this
package are documented in
engineering_notes_verify_template.md.
Each bug has a dedicated regression test in
tests/test_forge_happy.py, tests/test_bug5_unit.py,
tests/test_c6_threshold.py, tests/test_c7_citation_context.py,
tests/test_cache.py, tests/test_c10_reproducibility.py,
tests/test_c8_statistical_power.py, and
tests/test_c9_figure_caption.py. The meta-test
(_check_all_regressions.py) injects each bug in turn, asserts the
regression test catches it, and restores the source. 14/14 bugs
are caught.
The fix patterns (raw-string regex literals, brace-counting state
machines, non-letter-class regex, second/third-pass audit logic,
sentinel-character sub-string extraction, etc.) are all implemented
in src/tmaudit/forge.py and
src/tmaudit/templates/verify_TEMPLATE.py.
.github/workflows/ci.yml runs on every push and pull request.
It consists of seven jobs:
- pytest (matrix: Python 3.9, 3.10, 3.11, 3.12, 3.13 on
ubuntu-latest— every versionrequires-python >=3.9claims) — runs the 246-test suite and uploads JUnit XML artifacts. - meta-test (Python 3.11) — runs
_check_all_regressions.pyto verify that each of the 14 regression tests actually catches its target bug, then runs_check_pyz_fresh.pyto confirm the committedtmaudit.pyzstill matchessrc/tmaudit/, and finally builds the archive and runs it as a smoke test. - validate-community-files (Python 3.11) — runs
_check_github_templates.pyto verify the.github/directory structure (4 issue templates, PR template, CODEOWNERS, dependabot.yml, CODE_OF_CONDUCT, SUPPORT, workflows). - audit-all (Python 3.11) — runs
tmaudit audit-all --start 1 --end 5 --output audit-report.md --no-cacheand uploads the Markdown report as a CI artefact. - lint (Python 3.11) — runs
pyflakesonsrc/tmaudit/andtests/. - mutation-control (Python 3.11) — proves the pytest and lint
gates can actually fail: injects a known mutation, asserts both
gates go red, then asserts a deliberately neutralised gate (wrapped
in
|| true) is rejected as a negative control. - secret-scan (Python 3.11) — scans the full history with
gitleaks, so a committed credential fails the build.
To run the same checks locally before pushing:
pip install -e . pytest pyflakes
pip install -e ./tmaudit_example_plugin
pytest
python _check_all_regressions.py
python _check_pyz_fresh.py
python _build_pyz.py
python tmaudit.pyz list
python -m pyflakes src/tmaudit/ tests/pytest runs the full suite: 246 passed, 3 skipped on a machine
without the research tree — the two @pytest.mark.slow end-to-end audit
tests require the real PAPER<N>_CONSOLIDATED papers and auto-skip when
the directory is absent (point TMAUDIT_RESEARCH_DIR at the tree to run
them). Scope note: CI runs pytest -m "not slow", which deselects
those two and reports 246 passed, 1 skipped, 2 deselected — both
lines above are the same 249 collected tests, differing only in the two
deselected slow tests.
The preferred tool for local CI simulation is nektos/act:
act -l # list jobs
act -j pytest # run the pytest job
act -j meta-test # run the meta-test job (after pytest)
act -j lint # run the lint jobact requires act installed and a working Docker daemon.
If either is unavailable (e.g., on a restricted sandbox machine),
this repo ships two fallback simulators:
| Script | act equivalent |
What it does |
|---|---|---|
_act_dryrun.py |
act -n (dry-run) |
Parses ci.yml, expands the matrix, prints what each step would do. No commands are executed. |
_act_simulate.py |
act (real run) |
Same as dryrun, but actually runs the run: steps via subprocess. Implements stubs for actions/checkout@v4, actions/setup-python@v5, actions/upload-artifact@v4. |
python _act_dryrun.py # show what would be executed
python _act_dryrun.py --list # show jobs only
python _act_simulate.py --list # list jobs (act -l equivalent)
python _act_simulate.py --job pytest # run pytest job_act_dryrun.py was used to validate the workflow before
pushing. Results:
- ✅
ci.ymlis valid YAML (parsed by PyYAML) - ✅ 7 jobs (a 5-version pytest matrix plus 6 single-run jobs)
- ✅
meta-testcorrectly depends onpytest(Kahn's algorithm topo sort) - ✅
matrix.python-versionsubstituted correctly into${{ ... }}expressions - ✅
if: always()preserved on the upload-artifact step - ✅
with:arguments (e.g.,cache: pip,cache-dependency-path: pyproject.toml) preserved
The CI workflow's run: blocks are identical to commands
we have already executed successfully on the host:
| CI step | Local result |
|---|---|
python -m pip install -e . |
tmaudit installed ✅ |
python -m pip install pytest scipy |
deps installed ✅ |
python -m pytest -m "not slow" |
246 passed, 1 skipped in ~5s ✅ |
python _check_all_regressions.py |
14/14 bugs caught ✅ |
python _check_github_templates.py |
44/44 OK ✅ |
python -m src.tmaudit audit-all ... |
audit-report.md generated ✅ |
python -m src.tmaudit audit-all ... |
report uploaded as artefact ✅ |
python _build_pyz.py |
tmaudit.pyz built ✅ |
python tmaudit.pyz list |
5 papers listed ✅ |
python -m pyflakes src/tmaudit/ tests/ |
0 errors ✅ |
actions/setup-python@v5cannot actually install a different Python version (the host has only one Python; the matrix includes 3.9–3.13). The simulator prints a warning but does not fail.actions/upload-artifact@v4is simulated by copying files to_act_run/artifacts/<name>/.- The Docker isolation that real
actprovides is not present; steps run on the host shell.
To enforce CI validation before every push:
# .git/hooks/pre-push
python _check_yaml.py && python _act_dryrun.pyThe _check_yaml.py script validates ci.yml is parseable
and has the expected structure (catches YAML syntax errors
before pushing).
For the full simulation transcript (including the 5 sandbox
issues encountered and how they were worked around), see
act_summary.md.