mathfmt-tex 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.
Files changed (203) hide show
  1. mathfmt_tex-0.1.0/.github/workflows/wheels.yml +125 -0
  2. mathfmt_tex-0.1.0/.gitignore +9 -0
  3. mathfmt_tex-0.1.0/.phalanix/rig.nix +123 -0
  4. mathfmt_tex-0.1.0/.phalanix/roles/lead.md +46 -0
  5. mathfmt_tex-0.1.0/.phalanix/roles/question.md +22 -0
  6. mathfmt_tex-0.1.0/.phalanix/roles/review.md +41 -0
  7. mathfmt_tex-0.1.0/.phalanix/roles/task.md +41 -0
  8. mathfmt_tex-0.1.0/.phalanix/roles/traits/reports.md +37 -0
  9. mathfmt_tex-0.1.0/.pre-commit-hooks.yaml +29 -0
  10. mathfmt_tex-0.1.0/CLAUDE.md +143 -0
  11. mathfmt_tex-0.1.0/Cargo.lock +457 -0
  12. mathfmt_tex-0.1.0/Cargo.toml +44 -0
  13. mathfmt_tex-0.1.0/PKG-INFO +401 -0
  14. mathfmt_tex-0.1.0/README.md +387 -0
  15. mathfmt_tex-0.1.0/crates/mathfmt-core/Cargo.toml +15 -0
  16. mathfmt_tex-0.1.0/crates/mathfmt-core/data/alphabets.tsv +1023 -0
  17. mathfmt_tex-0.1.0/crates/mathfmt-core/data/defined.tsv +3001 -0
  18. mathfmt_tex-0.1.0/crates/mathfmt-core/data/greek.tsv +44 -0
  19. mathfmt_tex-0.1.0/crates/mathfmt-core/data/latex.tsv +336 -0
  20. mathfmt_tex-0.1.0/crates/mathfmt-core/data/packages.tsv +402 -0
  21. mathfmt_tex-0.1.0/crates/mathfmt-core/data/signatures.tsv +185 -0
  22. mathfmt_tex-0.1.0/crates/mathfmt-core/data/unicode-math.tsv +2450 -0
  23. mathfmt_tex-0.1.0/crates/mathfmt-core/src/atom.rs +150 -0
  24. mathfmt_tex-0.1.0/crates/mathfmt-core/src/config.rs +535 -0
  25. mathfmt_tex-0.1.0/crates/mathfmt-core/src/declare.rs +128 -0
  26. mathfmt_tex-0.1.0/crates/mathfmt-core/src/diag.rs +143 -0
  27. mathfmt_tex-0.1.0/crates/mathfmt-core/src/lex.rs +231 -0
  28. mathfmt_tex-0.1.0/crates/mathfmt-core/src/lib.rs +980 -0
  29. mathfmt_tex-0.1.0/crates/mathfmt-core/src/macros.rs +222 -0
  30. mathfmt_tex-0.1.0/crates/mathfmt-core/src/notation.rs +1044 -0
  31. mathfmt_tex-0.1.0/crates/mathfmt-core/src/parse.rs +1283 -0
  32. mathfmt_tex-0.1.0/crates/mathfmt-core/src/print.rs +826 -0
  33. mathfmt_tex-0.1.0/crates/mathfmt-core/src/project.rs +919 -0
  34. mathfmt_tex-0.1.0/crates/mathfmt-core/src/resolve.rs +710 -0
  35. mathfmt_tex-0.1.0/crates/mathfmt-core/src/signatures.rs +158 -0
  36. mathfmt_tex-0.1.0/crates/mathfmt-core/src/table.rs +481 -0
  37. mathfmt_tex-0.1.0/docs/REVIEW-2026-11-09.md +184 -0
  38. mathfmt_tex-0.1.0/docs/superpowers/plans/2026-11-05-mathfmt-v0.md +126 -0
  39. mathfmt_tex-0.1.0/docs/superpowers/specs/2026-11-05-mathfmt-design.md +1135 -0
  40. mathfmt_tex-0.1.0/flake.lock +27 -0
  41. mathfmt_tex-0.1.0/flake.nix +240 -0
  42. mathfmt_tex-0.1.0/pyproject.toml +34 -0
  43. mathfmt_tex-0.1.0/src/main.rs +895 -0
  44. mathfmt_tex-0.1.0/src/verify.rs +1047 -0
  45. mathfmt_tex-0.1.0/tests/cli.rs +648 -0
  46. mathfmt_tex-0.1.0/tests/dev.sh +40 -0
  47. mathfmt_tex-0.1.0/tests/fixtures/amsfonts-stubs/args +2 -0
  48. mathfmt_tex-0.1.0/tests/fixtures/amsfonts-stubs/expected.tex +1 -0
  49. mathfmt_tex-0.1.0/tests/fixtures/amsfonts-stubs/input.tex +1 -0
  50. mathfmt_tex-0.1.0/tests/fixtures/basic-to-ascii/args +2 -0
  51. mathfmt_tex-0.1.0/tests/fixtures/basic-to-ascii/expected.tex +2 -0
  52. mathfmt_tex-0.1.0/tests/fixtures/basic-to-ascii/input.tex +2 -0
  53. mathfmt_tex-0.1.0/tests/fixtures/basic-to-unicode/expected.tex +10 -0
  54. mathfmt_tex-0.1.0/tests/fixtures/basic-to-unicode/input.tex +10 -0
  55. mathfmt_tex-0.1.0/tests/fixtures/bb-lowercase/expected.tex +1 -0
  56. mathfmt_tex-0.1.0/tests/fixtures/bb-lowercase/input.tex +1 -0
  57. mathfmt_tex-0.1.0/tests/fixtures/bb-lowercase-ascii/args +2 -0
  58. mathfmt_tex-0.1.0/tests/fixtures/bb-lowercase-ascii/expected.tex +1 -0
  59. mathfmt_tex-0.1.0/tests/fixtures/bb-lowercase-ascii/input.tex +1 -0
  60. mathfmt_tex-0.1.0/tests/fixtures/bf-greek-pdflatex/expected.tex +1 -0
  61. mathfmt_tex-0.1.0/tests/fixtures/bf-greek-pdflatex/expected.verify +1 -0
  62. mathfmt_tex-0.1.0/tests/fixtures/bf-greek-pdflatex/input.tex +1 -0
  63. mathfmt_tex-0.1.0/tests/fixtures/definitions-untouched/expected.tex +3 -0
  64. mathfmt_tex-0.1.0/tests/fixtures/definitions-untouched/input.tex +3 -0
  65. mathfmt_tex-0.1.0/tests/fixtures/ensuremath-macro/expected.tex +2 -0
  66. mathfmt_tex-0.1.0/tests/fixtures/ensuremath-macro/input.tex +2 -0
  67. mathfmt_tex-0.1.0/tests/fixtures/group-as-script-operand/expected.tex +1 -0
  68. mathfmt_tex-0.1.0/tests/fixtures/group-as-script-operand/input.tex +1 -0
  69. mathfmt_tex-0.1.0/tests/fixtures/macros-folded/expected.tex +2 -0
  70. mathfmt_tex-0.1.0/tests/fixtures/macros-folded/input.tex +2 -0
  71. mathfmt_tex-0.1.0/tests/fixtures/macros-folded/mathfmt.toml +8 -0
  72. mathfmt_tex-0.1.0/tests/fixtures/macros-inlined/defs.sty +6 -0
  73. mathfmt_tex-0.1.0/tests/fixtures/macros-inlined/expected.tex +2 -0
  74. mathfmt_tex-0.1.0/tests/fixtures/macros-inlined/input.tex +2 -0
  75. mathfmt_tex-0.1.0/tests/fixtures/macros-inlined/mathfmt.toml +1 -0
  76. mathfmt_tex-0.1.0/tests/fixtures/no-latex-spelling/args +2 -0
  77. mathfmt_tex-0.1.0/tests/fixtures/no-latex-spelling/expected.stderr +1 -0
  78. mathfmt_tex-0.1.0/tests/fixtures/no-latex-spelling/expected.tex +1 -0
  79. mathfmt_tex-0.1.0/tests/fixtures/no-latex-spelling/expected.verify +1 -0
  80. mathfmt_tex-0.1.0/tests/fixtures/no-latex-spelling/input.tex +1 -0
  81. mathfmt_tex-0.1.0/tests/fixtures/notation-braced-literal/args +2 -0
  82. mathfmt_tex-0.1.0/tests/fixtures/notation-braced-literal/expected.tex +2 -0
  83. mathfmt_tex-0.1.0/tests/fixtures/notation-braced-literal/input.tex +2 -0
  84. mathfmt_tex-0.1.0/tests/fixtures/notation-braced-literal/mathfmt.toml +5 -0
  85. mathfmt_tex-0.1.0/tests/fixtures/notation-comment-in-match/expected.tex +4 -0
  86. mathfmt_tex-0.1.0/tests/fixtures/notation-comment-in-match/input.tex +4 -0
  87. mathfmt_tex-0.1.0/tests/fixtures/notation-comment-in-match/mathfmt.toml +5 -0
  88. mathfmt_tex-0.1.0/tests/fixtures/notation-form-keeps-its-macro/expected.tex +2 -0
  89. mathfmt_tex-0.1.0/tests/fixtures/notation-form-keeps-its-macro/input.tex +2 -0
  90. mathfmt_tex-0.1.0/tests/fixtures/notation-form-keeps-its-macro/mathfmt.toml +5 -0
  91. mathfmt_tex-0.1.0/tests/fixtures/notation-hole-after-control-word/expected.tex +2 -0
  92. mathfmt_tex-0.1.0/tests/fixtures/notation-hole-after-control-word/input.tex +2 -0
  93. mathfmt_tex-0.1.0/tests/fixtures/notation-hole-after-control-word/mathfmt.toml +5 -0
  94. mathfmt_tex-0.1.0/tests/fixtures/notation-hole-control-space/args +2 -0
  95. mathfmt_tex-0.1.0/tests/fixtures/notation-hole-control-space/expected.tex +2 -0
  96. mathfmt_tex-0.1.0/tests/fixtures/notation-hole-control-space/input.tex +3 -0
  97. mathfmt_tex-0.1.0/tests/fixtures/notation-hole-control-space/mathfmt.toml +5 -0
  98. mathfmt_tex-0.1.0/tests/fixtures/notation-nested-delimiters/args +2 -0
  99. mathfmt_tex-0.1.0/tests/fixtures/notation-nested-delimiters/expected.tex +2 -0
  100. mathfmt_tex-0.1.0/tests/fixtures/notation-nested-delimiters/input.tex +2 -0
  101. mathfmt_tex-0.1.0/tests/fixtures/notation-nested-delimiters/mathfmt.toml +5 -0
  102. mathfmt_tex-0.1.0/tests/fixtures/notation-script-hole/expected.tex +2 -0
  103. mathfmt_tex-0.1.0/tests/fixtures/notation-script-hole/input.tex +2 -0
  104. mathfmt_tex-0.1.0/tests/fixtures/notation-script-hole/mathfmt.toml +5 -0
  105. mathfmt_tex-0.1.0/tests/fixtures/notation-script-operand/expected.tex +2 -0
  106. mathfmt_tex-0.1.0/tests/fixtures/notation-script-operand/input.tex +2 -0
  107. mathfmt_tex-0.1.0/tests/fixtures/notation-script-operand/mathfmt.toml +5 -0
  108. mathfmt_tex-0.1.0/tests/fixtures/notation-script-operand-newline/expected.tex +4 -0
  109. mathfmt_tex-0.1.0/tests/fixtures/notation-script-operand-newline/input.tex +4 -0
  110. mathfmt_tex-0.1.0/tests/fixtures/notation-script-operand-newline/mathfmt.toml +5 -0
  111. mathfmt_tex-0.1.0/tests/fixtures/notation-spaced-form/expected.tex +2 -0
  112. mathfmt_tex-0.1.0/tests/fixtures/notation-spaced-form/expected.verify +1 -0
  113. mathfmt_tex-0.1.0/tests/fixtures/notation-spaced-form/input.tex +2 -0
  114. mathfmt_tex-0.1.0/tests/fixtures/notation-spaced-form/mathfmt.toml +9 -0
  115. mathfmt_tex-0.1.0/tests/fixtures/notation-standing-group-in-hole/args +2 -0
  116. mathfmt_tex-0.1.0/tests/fixtures/notation-standing-group-in-hole/expected.tex +2 -0
  117. mathfmt_tex-0.1.0/tests/fixtures/notation-standing-group-in-hole/input.tex +2 -0
  118. mathfmt_tex-0.1.0/tests/fixtures/notation-standing-group-in-hole/mathfmt.toml +5 -0
  119. mathfmt_tex-0.1.0/tests/fixtures/notation-styled-literal/args +2 -0
  120. mathfmt_tex-0.1.0/tests/fixtures/notation-styled-literal/expected.tex +3 -0
  121. mathfmt_tex-0.1.0/tests/fixtures/notation-styled-literal/input.tex +3 -0
  122. mathfmt_tex-0.1.0/tests/fixtures/notation-styled-literal/mathfmt.toml +9 -0
  123. mathfmt_tex-0.1.0/tests/fixtures/notation-to-expanded/expected.tex +2 -0
  124. mathfmt_tex-0.1.0/tests/fixtures/notation-to-expanded/input.tex +2 -0
  125. mathfmt_tex-0.1.0/tests/fixtures/notation-to-expanded/mathfmt.toml +6 -0
  126. mathfmt_tex-0.1.0/tests/fixtures/notation-to-macro/expected.tex +2 -0
  127. mathfmt_tex-0.1.0/tests/fixtures/notation-to-macro/input.tex +2 -0
  128. mathfmt_tex-0.1.0/tests/fixtures/notation-to-macro/mathfmt.toml +8 -0
  129. mathfmt_tex-0.1.0/tests/fixtures/notation-unclosed-group/expected.stderr +4 -0
  130. mathfmt_tex-0.1.0/tests/fixtures/notation-unclosed-group/expected.tex +4 -0
  131. mathfmt_tex-0.1.0/tests/fixtures/notation-unclosed-group/expected.verify +1 -0
  132. mathfmt_tex-0.1.0/tests/fixtures/notation-unclosed-group/input.tex +4 -0
  133. mathfmt_tex-0.1.0/tests/fixtures/notation-unclosed-group/mathfmt.toml +5 -0
  134. mathfmt_tex-0.1.0/tests/fixtures/operator-words-untouched/expected.tex +2 -0
  135. mathfmt_tex-0.1.0/tests/fixtures/operator-words-untouched/input.tex +2 -0
  136. mathfmt_tex-0.1.0/tests/fixtures/prime-is-apostrophe/expected.tex +1 -0
  137. mathfmt_tex-0.1.0/tests/fixtures/prime-is-apostrophe/input.tex +1 -0
  138. mathfmt_tex-0.1.0/tests/fixtures/rename-frak-to-cal/args +5 -0
  139. mathfmt_tex-0.1.0/tests/fixtures/rename-frak-to-cal/expected.tex +1 -0
  140. mathfmt_tex-0.1.0/tests/fixtures/rename-frak-to-cal/expected.verify +1 -0
  141. mathfmt_tex-0.1.0/tests/fixtures/rename-frak-to-cal/input.tex +1 -0
  142. mathfmt_tex-0.1.0/tests/fixtures/roundtrip-paper-snippet/expected.tex +6 -0
  143. mathfmt_tex-0.1.0/tests/fixtures/roundtrip-paper-snippet/input.tex +6 -0
  144. mathfmt_tex-0.1.0/tests/fixtures/roundtrip-paper-snippet/roundtrip +0 -0
  145. mathfmt_tex-0.1.0/tests/fixtures/script-braces/expected.tex +1 -0
  146. mathfmt_tex-0.1.0/tests/fixtures/script-braces/input.tex +1 -0
  147. mathfmt_tex-0.1.0/tests/fixtures/structured-group-before-relax/expected.tex +1 -0
  148. mathfmt_tex-0.1.0/tests/fixtures/structured-group-before-relax/input.tex +1 -0
  149. mathfmt_tex-0.1.0/tests/fixtures/structured-group-before-script/expected.tex +1 -0
  150. mathfmt_tex-0.1.0/tests/fixtures/structured-group-before-script/input.tex +1 -0
  151. mathfmt_tex-0.1.0/tests/fixtures/structured-group-before-script-ascii/args +2 -0
  152. mathfmt_tex-0.1.0/tests/fixtures/structured-group-before-script-ascii/expected.tex +1 -0
  153. mathfmt_tex-0.1.0/tests/fixtures/structured-group-before-script-ascii/expected.verify +1 -0
  154. mathfmt_tex-0.1.0/tests/fixtures/structured-group-before-script-ascii/input.tex +1 -0
  155. mathfmt_tex-0.1.0/tests/fixtures/structured-group-before-sp/expected.tex +2 -0
  156. mathfmt_tex-0.1.0/tests/fixtures/structured-group-before-sp/input.tex +2 -0
  157. mathfmt_tex-0.1.0/tests/fixtures/style-distribution/expected.tex +1 -0
  158. mathfmt_tex-0.1.0/tests/fixtures/style-distribution/input.tex +1 -0
  159. mathfmt_tex-0.1.0/tests/fixtures/style-distribution-ascii/args +2 -0
  160. mathfmt_tex-0.1.0/tests/fixtures/style-distribution-ascii/expected.tex +1 -0
  161. mathfmt_tex-0.1.0/tests/fixtures/style-distribution-ascii/input.tex +1 -0
  162. mathfmt_tex-0.1.0/tests/fixtures/style-distribution-repairs/args +2 -0
  163. mathfmt_tex-0.1.0/tests/fixtures/style-distribution-repairs/expected.tex +1 -0
  164. mathfmt_tex-0.1.0/tests/fixtures/style-distribution-repairs/expected.verify +1 -0
  165. mathfmt_tex-0.1.0/tests/fixtures/style-distribution-repairs/input.tex +1 -0
  166. mathfmt_tex-0.1.0/tests/fixtures/styled-runs/args +2 -0
  167. mathfmt_tex-0.1.0/tests/fixtures/styled-runs/expected.tex +1 -0
  168. mathfmt_tex-0.1.0/tests/fixtures/styled-runs/input.tex +1 -0
  169. mathfmt_tex-0.1.0/tests/fixtures/text-nesting/expected.tex +1 -0
  170. mathfmt_tex-0.1.0/tests/fixtures/text-nesting/input.tex +1 -0
  171. mathfmt_tex-0.1.0/tests/fixtures/unbraced-argument/expected.tex +1 -0
  172. mathfmt_tex-0.1.0/tests/fixtures/unbraced-argument/input.tex +1 -0
  173. mathfmt_tex-0.1.0/tests/fixtures/unbraced-argument-ascii/args +2 -0
  174. mathfmt_tex-0.1.0/tests/fixtures/unbraced-argument-ascii/expected.tex +1 -0
  175. mathfmt_tex-0.1.0/tests/fixtures/unbraced-argument-ascii/expected.verify +1 -0
  176. mathfmt_tex-0.1.0/tests/fixtures/unbraced-argument-ascii/input.tex +1 -0
  177. mathfmt_tex-0.1.0/tests/fixtures/url-arguments-skipped/expected.tex +1 -0
  178. mathfmt_tex-0.1.0/tests/fixtures/url-arguments-skipped/input.tex +1 -0
  179. mathfmt_tex-0.1.0/tests/fixtures/verbatim-skipped/expected.tex +8 -0
  180. mathfmt_tex-0.1.0/tests/fixtures/verbatim-skipped/input.tex +8 -0
  181. mathfmt_tex-0.1.0/tests/golden.rs +211 -0
  182. mathfmt_tex-0.1.0/tests/pre-commit-e2e.sh +50 -0
  183. mathfmt_tex-0.1.0/tests/project.rs +82 -0
  184. mathfmt_tex-0.1.0/tests/projects/bbold/expected/main.tex +6 -0
  185. mathfmt_tex-0.1.0/tests/projects/bbold/expected.declarations +7 -0
  186. mathfmt_tex-0.1.0/tests/projects/bbold/main.tex +6 -0
  187. mathfmt_tex-0.1.0/tests/projects/bbold/mathfmt.toml +2 -0
  188. mathfmt_tex-0.1.0/tests/projects/paper/expected/main.tex +8 -0
  189. mathfmt_tex-0.1.0/tests/projects/paper/expected/sections/method.tex +2 -0
  190. mathfmt_tex-0.1.0/tests/projects/paper/expected/sections/results.tex +3 -0
  191. mathfmt_tex-0.1.0/tests/projects/paper/expected.declarations +18 -0
  192. mathfmt_tex-0.1.0/tests/projects/paper/main.tex +8 -0
  193. mathfmt_tex-0.1.0/tests/projects/paper/mathfmt.toml +1 -0
  194. mathfmt_tex-0.1.0/tests/projects/paper/paper.sty +6 -0
  195. mathfmt_tex-0.1.0/tests/projects/paper/sections/method.tex +2 -0
  196. mathfmt_tex-0.1.0/tests/projects/paper/sections/results.tex +3 -0
  197. mathfmt_tex-0.1.0/tests/projects/unicode-math/expected/main.tex +8 -0
  198. mathfmt_tex-0.1.0/tests/projects/unicode-math/expected.declarations +0 -0
  199. mathfmt_tex-0.1.0/tests/projects/unicode-math/main.tex +8 -0
  200. mathfmt_tex-0.1.0/tests/projects/unicode-math/mathfmt.toml +2 -0
  201. mathfmt_tex-0.1.0/tests/verify.rs +608 -0
  202. mathfmt_tex-0.1.0/tools/gen-packages.py +241 -0
  203. mathfmt_tex-0.1.0/tools/gen-table.py +157 -0
@@ -0,0 +1,125 @@
1
+ # Wheels of mathfmt-tex for every platform pre-commit users run, built on
2
+ # the GitHub mirror (Forgejo pushes to it). A wheel holds the compiled
3
+ # `mathfmt` binary only (pyproject.toml), so installing it needs no Rust.
4
+ # A tag `v*` also publishes to PyPI through trusted publishing: the PyPI
5
+ # project must name this repository and this workflow as its publisher.
6
+ #
7
+ # Every job is skipped anywhere but github.com, so a Forgejo instance that
8
+ # reads .github/workflows does not try to run it.
9
+
10
+ name: wheels
11
+
12
+ on:
13
+ push:
14
+ branches: [main]
15
+ tags: ["v*"]
16
+ pull_request:
17
+ workflow_dispatch:
18
+
19
+ permissions:
20
+ contents: read
21
+
22
+ jobs:
23
+ linux:
24
+ if: github.server_url == 'https://github.com'
25
+ runs-on: ubuntu-latest
26
+ strategy:
27
+ matrix:
28
+ target: [x86_64, aarch64]
29
+ libc: [manylinux, musllinux]
30
+ steps:
31
+ - uses: actions/checkout@v4
32
+ - uses: PyO3/maturin-action@v1
33
+ with:
34
+ target: ${{ matrix.target }}
35
+ manylinux: ${{ matrix.libc == 'manylinux' && 'auto' || 'musllinux_1_2' }}
36
+ args: --release --locked --out dist
37
+ - name: Run the wheel's mathfmt
38
+ if: matrix.target == 'x86_64' && matrix.libc == 'manylinux'
39
+ run: |
40
+ python3 -m venv /tmp/venv
41
+ /tmp/venv/bin/pip install dist/*.whl
42
+ /tmp/venv/bin/mathfmt --version
43
+ printf '%s\n' '$\alpha \in \mathbb{R}$' | /tmp/venv/bin/mathfmt | grep -Fx '$α ∈ ℝ$'
44
+ - uses: actions/upload-artifact@v4
45
+ with:
46
+ name: wheels-linux-${{ matrix.target }}-${{ matrix.libc }}
47
+ path: dist
48
+
49
+ macos:
50
+ if: github.server_url == 'https://github.com'
51
+ strategy:
52
+ matrix:
53
+ include:
54
+ - runner: macos-15-intel
55
+ target: x86_64
56
+ - runner: macos-15
57
+ target: aarch64
58
+ runs-on: ${{ matrix.runner }}
59
+ steps:
60
+ - uses: actions/checkout@v4
61
+ - uses: PyO3/maturin-action@v1
62
+ with:
63
+ target: ${{ matrix.target }}
64
+ args: --release --locked --out dist
65
+ - name: Run the wheel's mathfmt
66
+ run: |
67
+ python3 -m venv /tmp/venv
68
+ /tmp/venv/bin/pip install dist/*.whl
69
+ /tmp/venv/bin/mathfmt --version
70
+ printf '%s\n' '$\alpha \in \mathbb{R}$' | /tmp/venv/bin/mathfmt | grep -Fx '$α ∈ ℝ$'
71
+ - uses: actions/upload-artifact@v4
72
+ with:
73
+ name: wheels-macos-${{ matrix.target }}
74
+ path: dist
75
+
76
+ windows:
77
+ if: github.server_url == 'https://github.com'
78
+ runs-on: windows-latest
79
+ steps:
80
+ - uses: actions/checkout@v4
81
+ - uses: actions/setup-python@v5
82
+ with:
83
+ python-version: "3.x"
84
+ - uses: PyO3/maturin-action@v1
85
+ with:
86
+ target: x64
87
+ args: --release --locked --out dist
88
+ - name: Run the wheel's mathfmt
89
+ shell: bash
90
+ run: |
91
+ python -m venv venv
92
+ venv/Scripts/pip install dist/*.whl
93
+ venv/Scripts/mathfmt --version
94
+ - uses: actions/upload-artifact@v4
95
+ with:
96
+ name: wheels-windows-x64
97
+ path: dist
98
+
99
+ sdist:
100
+ if: github.server_url == 'https://github.com'
101
+ runs-on: ubuntu-latest
102
+ steps:
103
+ - uses: actions/checkout@v4
104
+ - uses: PyO3/maturin-action@v1
105
+ with:
106
+ command: sdist
107
+ args: --out dist
108
+ - uses: actions/upload-artifact@v4
109
+ with:
110
+ name: sdist
111
+ path: dist
112
+
113
+ publish:
114
+ if: github.server_url == 'https://github.com' && startsWith(github.ref, 'refs/tags/v')
115
+ needs: [linux, macos, windows, sdist]
116
+ runs-on: ubuntu-latest
117
+ environment: pypi
118
+ permissions:
119
+ id-token: write
120
+ steps:
121
+ - uses: actions/download-artifact@v4
122
+ with:
123
+ path: dist
124
+ merge-multiple: true
125
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,9 @@
1
+ /target
2
+ /result
3
+ /result-*
4
+ *.pdf
5
+ *.aux
6
+ *.log
7
+ /tests/fixtures/**/actual.tex
8
+ .direnv/
9
+ /.cargo-home
@@ -0,0 +1,123 @@
1
+ # The mathfmt rig: seats that develop mathfmt, in the loop that built
2
+ # phalanix v0.6.0. Bring it up from a checkout with
3
+ #
4
+ # phalanix up .phalanix/rig.nix
5
+ #
6
+ # `up` sends this one file to compute, the seat host, and evaluates it there
7
+ # (restricted: no imports, no reads), passing `phx` (phalanix's nix/phx.nix).
8
+ #
9
+ # Standing:
10
+ #
11
+ # lead one wave at a time: its spec, its plan and its ledger of
12
+ # rulings (docs/handoff/wave-NN.md); starts task, review and
13
+ # question seats with briefs, reads their summaries, chains
14
+ # tasks with --base, and tells the person what to land
15
+ #
16
+ # Templates, each spawned by lead (or the person) with a brief:
17
+ #
18
+ # task one task of the plan, test first, on its own branch
19
+ # review an adversarial review of one branch against its base;
20
+ # reports findings and never fixes them
21
+ # question one question about the code, answered
22
+ #
23
+ # Every kind runs on the strongest model, the person's rule for multi-agent
24
+ # work. Roles are repo: layers read from each seat's branch at every start,
25
+ # so .phalanix/ must be on main before the first seat starts. CLAUDE.md at
26
+ # the root is every seat's memory too: it holds the build, the tests and the
27
+ # person's rules, and the roles do not repeat it.
28
+ #
29
+ # The repository is jack/mathfmt on Forgejo, which is private: compute's
30
+ # daemon clones it and pushes submitted branches with the deploy key compute
31
+ # holds (secrets/phalanix-forge-forgejo in ~/dev/nix), which must be
32
+ # registered on the repository with write access. compute admits every seat
33
+ # user to its main nix daemon (nix.settings.allowed-users in ~/dev/nix). The
34
+ # dev shell vendors every crate from Cargo.lock (flake.nix, tests/dev.sh), so
35
+ # the seat proxy needs no crates.io entry: the nix daemon fetches the crates,
36
+ # and github.com with codeload (already allowed) serves the pinned nixpkgs
37
+ # when compute's store lacks it. Needs phalanix v0.6.0 or later.
38
+ { host ? { }, lib, phx, ... }:
39
+ let
40
+ repo = "ssh://forgejo@git.buttermilks.rocks/jack/mathfmt.git";
41
+ layer = path: "repo:.phalanix/roles/${path}.md";
42
+
43
+ # What every seat and template shares, given as each kind's own options.
44
+ common = {
45
+ inherit repo;
46
+ tool = "claude";
47
+ base = "main";
48
+ continuity = "resume";
49
+ model = "opus";
50
+ };
51
+ kind = traits: own: phx.kind traits (common // own);
52
+
53
+ # A seat's PATH may have no nix or bash; the roles and CLAUDE.md spell it out.
54
+ nix = "/run/current-system/sw/bin/nix";
55
+ bash = "/run/current-system/sw/bin/bash";
56
+
57
+ traits = {
58
+ reports = phx.trait { role = layer "traits/reports"; };
59
+ # The dev shell through tests/dev.sh (cargo against the vendored crates,
60
+ # clippy, rustfmt; the wrapper drops the seat's identity first), and the
61
+ # flake checks, which the nix daemon builds. This is a blanket
62
+ # pre-approval: anything wrapped in dev.sh skips the permission
63
+ # classifier, and dev.sh is a file the seat may edit on its own branch.
64
+ # Accepted, because building and testing is the seat's whole job. The
65
+ # second form is for a seat whose PATH has no bash for dev.sh's shebang.
66
+ builds = phx.trait {
67
+ permissions.allow = [
68
+ "Bash(repo/tests/dev.sh *)"
69
+ "Bash(${bash} repo/tests/dev.sh *)"
70
+ "Bash(${nix} build *)"
71
+ "Bash(${nix} flake check *)"
72
+ ];
73
+ };
74
+ };
75
+
76
+ rig = phx.merge [
77
+ {
78
+ seats = {
79
+ # lead reads summaries, not logs, so it does not build.
80
+ lead = kind [ ] {
81
+ role = layer "lead";
82
+ memory = "8G";
83
+ accept = "ask";
84
+ spawns = { task = 3; review = 2; question = 2; };
85
+ };
86
+ };
87
+ templates = {
88
+ task = kind [ traits.reports traits.builds ] {
89
+ role = layer "task";
90
+ memory = "8G";
91
+ accept = "auto";
92
+ spawns.question = 1;
93
+ };
94
+ review = kind [ traits.reports traits.builds ] {
95
+ role = layer "review";
96
+ memory = "8G";
97
+ accept = "auto";
98
+ };
99
+ question = kind [ traits.reports traits.builds ] {
100
+ role = layer "question";
101
+ memory = "8G";
102
+ accept = "auto";
103
+ };
104
+ };
105
+ }
106
+ ];
107
+
108
+ kinds = rig.seats // rig.templates;
109
+ both = a: b: [{ from = a; to = b; } { from = b; to = a; }];
110
+ in
111
+ rig // {
112
+ name = "mathfmt";
113
+
114
+ # Every seat: the verbs, and git in its clone (./repo).
115
+ permissions.allow = [ "Bash(phalanix *)" "Bash(git -C repo *)" ];
116
+
117
+ # lead talks to every other kind, both ways. A parent and its child reach
118
+ # each other without an edge, so a task's question needs none.
119
+ edges = rig.edges
120
+ ++ lib.concatMap (both "lead") (lib.attrNames (removeAttrs kinds [ "lead" ]));
121
+
122
+ commit.author_email = "mathfmt@phalanix.invalid";
123
+ }
@@ -0,0 +1,46 @@
1
+ You are lead, the mathfmt rig's controller. You run one wave of mathfmt development at a
2
+ time: you hold its spec, its plan and its ledger, and nothing below them. Each seat you start
3
+ holds less than you do, and more detail. CLAUDE.md (in your memory) has the build, the tests
4
+ and the person's rules.
5
+
6
+ **Each life.** Read your ledger, docs/handoff/wave-NN.md on your branch, before anything
7
+ else; it is your memory across lives, and the transcript is not. In your first life there is
8
+ no ledger yet: read docs/superpowers/ and ask the person what the first wave is. `phalanix
9
+ items --mine` and `phalanix inbox --ack` show what arrived while you were away.
10
+
11
+ **A wave** lands as one chain of branches. Each seat starts from the branch of the seat
12
+ before it, so the last seat's branch holds the whole wave. Run it in order:
13
+
14
+ 1. **Spec.** Talk the design through with the person in your pane. Write it as
15
+ docs/superpowers/specs/<date>-<topic>-design.md, or a dated amendment at the end of the
16
+ existing one, and get their approval.
17
+ 2. **Plan.** Write docs/superpowers/plans/<date>-<topic>.md: the tasks, each small enough for
18
+ one seat and one review, with the files it touches, the interfaces it produces and the
19
+ fixtures and tests that prove it. Tasks that share a file or an interface must agree on
20
+ it. Get the person's approval.
21
+ 3. **Land the spec and plan.** Start the ledger, docs/handoff/wave-NN.md (the next number):
22
+ the plan's path, then one line per event and every ruling. Commit, `phalanix file "wave
23
+ NN: spec and plan"`, `phalanix claim <item>`, `phalanix submit <item>`. Ask the person to
24
+ land your branch (`phalanix land mathfmt/lead`) and push main; then `phalanix sync --dir
25
+ repo` and check that origin/main has the plan. From here on your branch changes only the
26
+ ledger.
27
+ 4. **Tasks**, one at a time, in plan order. Write a brief file holding the task's full text,
28
+ the interfaces earlier tasks produced, your rulings, and what to report; `phalanix spawn
29
+ --template task --brief-file <file> --no-attach`, with `--base seats/mathfmt/<previous
30
+ task>` for every task after the first, and only once `phalanix items --json` shows that
31
+ seat's latest item done with `pushed_at` set. Then a review with `--base
32
+ seats/mathfmt/<task>` and a brief naming the task's brief, its report, the commit range,
33
+ the risks you see, the spec and plan. Rule on each finding; `phalanix close
34
+ <review>@mathfmt`. Findings go back as a new item (`phalanix file "fix round <r> for
35
+ <task>: ..."`, `phalanix offer <item> <task>@mathfmt`), at most five rounds. Copy each
36
+ task's counts and open points from its report into the ledger (attachments are deleted 14
37
+ days after close), then `phalanix close <task>@mathfmt`. A finding about a task a later
38
+ task has built on goes to a new task at the tip of the chain, never back down it.
39
+ 5. **Final review.** One review seat over the whole wave, main to the last branch; its brief
40
+ says it commits docs/REVIEW-<date>-waveNN.md and submits. Its findings go to a fix task.
41
+ 6. **Verification.** The last seat runs `repo/tests/dev.sh cargo test --workspace`, clippy,
42
+ `cargo fmt --check` and every flake check, writes the counts into the ledger, and submits.
43
+ Tell the person what to land, in order.
44
+
45
+ **Rulings** are yours (an ambiguity in the plan, a finding to leave, a test a task seat says
46
+ is wrong), one line each in the ledger. What you cannot rule on goes to the person.
@@ -0,0 +1,22 @@
1
+ You are a question seat of the mathfmt rig. You investigate one question from your brief,
2
+ answer it and stop. CLAUDE.md (in your memory) has the build, the tests and the person's
3
+ rules; the reports layer above says how to report.
4
+
5
+ **Your scope** is the question and the evidence for its answer. That covers the code, the
6
+ data tables, the fixtures, docs/ (the spec, the plans, the reviews, the handoffs), and small
7
+ checks in your clone, ./repo: git as `git -C repo ...`, anything else through
8
+ `repo/tests/dev.sh <command>` (a one-off `repo/tests/dev.sh cargo run -- --emit atoms` on a
9
+ scratch file is the usual check). You answer your parent, the seat that spawned you; when the
10
+ person spawned you, you answer lead. **You never carry** the wave's plan, another seat's
11
+ change or its review: what you need of your parent's context is in your brief, and anything
12
+ more you ask your parent for. You never change code beyond a scratch check you do not commit,
13
+ and you never spawn a seat.
14
+
15
+ **Answer** in the form your brief asks for, the answer first. Back it with file:line
16
+ references, or with a command and what it printed.
17
+ - A short answer is a message to your parent: `phalanix send <parent> "<answer>" --item
18
+ <item>`, then `phalanix done <item> --reason completed`.
19
+ - A longer one is a `report.md` attachment, as the reports layer says.
20
+ - When only a change or a long run can answer the question, say which one would, and stop.
21
+
22
+ **Commits.** You make none.
@@ -0,0 +1,41 @@
1
+ You are a review seat of the mathfmt rig: an adversarial reviewer of one branch. Your clone
2
+ starts on the branch under review (your spawn's base). Your brief names the commit range, the
3
+ task's brief and report, the wave's spec and plan, and the risks your parent sees. CLAUDE.md
4
+ (in your memory) has the build, the tests and the person's rules; the reports layer above says
5
+ how to report.
6
+
7
+ **Your scope** is the range in your brief and what it touches. **You never fix anything.** You
8
+ never commit code, push a fix or spawn a seat. You find what is wrong and show it; the task
9
+ seat fixes it, and your parent rules on what not to fix.
10
+
11
+ **Review** your clone, ./repo, against two questions. Run git as `git -C repo ...`, and any
12
+ build or test through the wrapper as `repo/tests/dev.sh <command>` (CLAUDE.md). The spec and
13
+ plan are on main, so they are in your clone too.
14
+ 1. **Does it do what the brief, spec and plan say?** Every requirement met, nothing missing,
15
+ nothing extra. A moved interface named in the report.
16
+ 2. **Is it right and maintainable?**
17
+ - Round trips: does the output go back through the other profile to the input? Is the
18
+ output idempotent? Are untouched bytes byte-identical?
19
+ - Edge inputs: `\$`, `\\(`, `%` inside display math, `$` inside `\text{}` inside `$…$`,
20
+ `\verb|$|`, a macro that expands to itself, an empty file, a file with no math.
21
+ - Tables, not code: a symbol added as a match arm instead of a data row.
22
+ - Tests that would fail if the code were wrong; a fixture whose expected.tex was copied
23
+ from the output without being read counts as no test.
24
+ - Readable by one person: names, comments and prose that say why.
25
+
26
+ **Prove what you claim.** Read the code at the lines you cite. Where a finding can be shown,
27
+ show it: a failing test or fixture you write in a scratch file (not committed), a command and
28
+ its output, or an input traced through the code. Separate CONFIRMED findings, which you have
29
+ shown, from PLAUSIBLE ones, which you have reasoned but not shown, and drop what you could
30
+ not support.
31
+
32
+ **Report.** Rank findings High, Medium or Low. Each gets the file and line; what is wrong, as
33
+ one sentence; the input or state that breaks it, and what happens; the fix you would expect.
34
+ End with a verdict: approve, or needs fixes. For a task or a fix round, the report is the
35
+ attachment the reports layer describes, and you finish with `phalanix done <item> --reason
36
+ completed`. For a whole wave, your brief says so: write the review as
37
+ docs/REVIEW-<date>-waveNN.md, commit it, attach the summary, and `phalanix submit <item>`.
38
+
39
+ **Commits** (the review document only). `area: what changed`, lowercase, a title of 60
40
+ characters or fewer, a body that says why, no model identifier, hooks never skipped. phalanix
41
+ sets your identity: never change git config. You never push.
@@ -0,0 +1,41 @@
1
+ You are a task seat of the mathfmt rig. You implement one task of a wave's plan, from the
2
+ brief your parent gave you, on your own branch. CLAUDE.md (in your memory) has the build, the
3
+ tests and the person's rules; the reports layer above says how to report.
4
+
5
+ **Your scope** is your task: its code, tests, fixtures, docs and runs. **You never carry**
6
+ the wave's plan beyond your task (lead's), another task's change, or a review of your own
7
+ work: a review seat does that after you report, and you never start one. The brief holds what
8
+ you need of the spec and plan; read the files it names, and ask your parent rather than guess.
9
+
10
+ **Work** in your clone, ./repo. Run git as `git -C repo ...`, and every build and test
11
+ through the wrapper as `repo/tests/dev.sh <command>` (CLAUDE.md says why; never a bare `nix
12
+ develop`; cargo runs offline against the vendored crates, so a new dependency needs
13
+ Cargo.lock committed before the shell sees it).
14
+ - Test first: write the test, or the fixture under tests/fixtures/<case>/, that pins the
15
+ brief's behaviour, watch it fail, make it pass. A bug you find on the way becomes a fixture.
16
+ - A symbol, an alias or a command signature is a row in crates/mathfmt-core/data/latex.tsv
17
+ or signatures.tsv, never a match arm; unicode-math.tsv and alphabets.tsv are generated by
18
+ tools/gen-table.py and edited only by rerunning it.
19
+ - Match the code and the prose around you: module boundaries from the spec, the fixture
20
+ layout, the comment density. When your change moves an interface, say so in the report.
21
+ - Before you report, run `repo/tests/dev.sh cargo test --workspace`, `repo/tests/dev.sh
22
+ cargo clippy --workspace --all-targets -- -D warnings` and `repo/tests/dev.sh cargo fmt
23
+ --check`. A flake check (`/run/current-system/sw/bin/nix build
24
+ ./repo#checks.x86_64-linux.<name>`) only when your change touches flake.nix or Cargo.lock.
25
+ - Never weaken a test or edit an expected.tex to make it pass. When a test is wrong, say why
26
+ in the report.
27
+
28
+ **Fix rounds.** After your report, a fix round may arrive as a new item, naming findings and
29
+ your parent's rulings on them. Fix exactly those findings, with tests where they apply,
30
+ commit, report on that item, and submit it. Don't reopen what the round did not name.
31
+
32
+ **Help.** You may have one question seat live, spawned with a brief that says the question
33
+ and the form of answer you need: `phalanix spawn --template question --brief-file <file>
34
+ --no-attach`.
35
+
36
+ **Result.** Your branch carries the change, its tests and the docs it moves, in commits that
37
+ each build. Report as the reports layer says, then `phalanix submit <item>`.
38
+
39
+ **Commits.** `area: what changed`, lowercase, a title of 60 characters or fewer and a body
40
+ that says why; no model identifier anywhere; never skip hooks. phalanix sets your identity:
41
+ never change git config. You never push; the daemon pushes your branch when you submit.
@@ -0,0 +1,37 @@
1
+ How you report. You hold one piece of work, and its details go to exactly one seat: your parent.
2
+
3
+ **Your parent** is the seat that spawned you. `phalanix ps --all` shows it in the PARENT
4
+ column, and your brief item's ref names the item it holds (`item:<n>`) or the seat itself. A
5
+ seat the person spawned has no parent, and reports to lead. Parent and child reach each other
6
+ without an edge.
7
+
8
+ **Start.** `phalanix items --mine`. Your brief, and any later fix round, is usually yours
9
+ already: you accept automatically, so the ledger has taken it for you. `phalanix accept
10
+ <item>` only an item still shown as offered (`claim` takes only an open item no one was
11
+ offered). Read the brief whole. If it is unclear, ask your parent before you change
12
+ anything.
13
+
14
+ **Details go in a report attachment.** Write `report.md`:
15
+ - what you did;
16
+ - the commits, as `git log --oneline` shows them;
17
+ - what you ran and its counts, as cargo prints them (`test result: ok. N passed`);
18
+ - anything you doubted or left open.
19
+ Attach it: `phalanix attach <item> report.md --name report-<n>.md`, with `<n>` the item
20
+ number, since an item holds one attachment per name.
21
+
22
+ **Finishing.** Attach first: `done` is final, and nothing can be attached after it. Then:
23
+ - When your branch carries the result, `phalanix submit <item>`. The daemon pushes it soon
24
+ after, as seats/mathfmt/<your seat>, and nothing leaves your clone before.
25
+ - When the result is attachments and messages only, `phalanix done <item> --reason
26
+ completed`.
27
+ - When you are stuck, `phalanix done <item> --reason blocked-on --target item:<n>`, or
28
+ `--reason escalated --target person` when only the person can unblock you.
29
+ - When it is not your work after all, attach what you have and hand the item back:
30
+ `phalanix handoff <item> <parent>`.
31
+ Then tell your parent in a line or two: `phalanix send <parent> "<what is ready>" --item
32
+ <item>`. A message names things; it never carries a log, a diff or a table. Read mail with
33
+ `phalanix inbox --ack`.
34
+
35
+ **Your own children**, if you spawn any, report to you the same way. Read their reports,
36
+ fetch only the attachment you need (`phalanix fetch <item> <name>`), and once a child's item is
37
+ done, `phalanix close <child>@mathfmt` to free its slot.
@@ -0,0 +1,29 @@
1
+ # Use this repository as a pre-commit hook:
2
+ #
3
+ # repos:
4
+ # - repo: ssh://forgejo@git.buttermilks.rocks/jack/mathfmt.git
5
+ # rev: v0.1.0
6
+ # hooks:
7
+ # - id: mathfmt # rewrites staged .tex/.sty files to the profile
8
+ # # - id: mathfmt-check # or: fail the commit, change nothing
9
+ #
10
+ # `language: rust` builds the binary with cargo on first use. An installed
11
+ # binary on PATH works the same way with `language: system` in your own
12
+ # .pre-commit-config.yaml.
13
+ - id: mathfmt
14
+ name: mathfmt
15
+ description: enforce math-notation conventions in LaTeX source
16
+ entry: mathfmt
17
+ language: rust
18
+ args: ["--write"]
19
+ files: \.(tex|sty)$
20
+ pass_filenames: true
21
+
22
+ - id: mathfmt-check
23
+ name: mathfmt (check)
24
+ description: fail when math notation does not match the profile; change nothing
25
+ entry: mathfmt
26
+ language: rust
27
+ args: ["--check"]
28
+ files: \.(tex|sty)$
29
+ pass_filenames: true
@@ -0,0 +1,143 @@
1
+ # mathfmt
2
+
3
+ A filter that rewrites the math notation in LaTeX source to a configured
4
+ profile: many spellings in, one spelling out, nothing else touched. The design
5
+ is `docs/superpowers/specs/2026-11-05-mathfmt-design.md`; this file is how to
6
+ work in the repository. It is loaded by every seat of the mathfmt rig
7
+ (`.phalanix/rig.nix`) and by anyone else working here.
8
+
9
+ ## Where things are
10
+
11
+ - `docs/superpowers/specs/`: the design, one dated file; amendments go at its
12
+ end, dated. `docs/superpowers/plans/`: one plan per wave, a task per seat.
13
+ - `docs/handoff/wave-NN.md`: lead's ledger for a wave (rulings, one line per
14
+ event, a paragraph per task copied from its report). `docs/REVIEW-*.md`: a
15
+ wave's adversarial review.
16
+ - `crates/mathfmt-core/`: the library, one module per pipeline stage (scan,
17
+ lex, parse, macros, notation, resolve, profile, print, config, diag).
18
+ `notation.rs` holds the pattern matcher and the rewrite for notations with
19
+ holes (`[notations]` in the config). `project.rs` reads a root document
20
+ (the files it inputs, the packages and local definition files of its
21
+ preamble, the engine) and `declare.rs` holds what `mathfmt declarations`
22
+ prints. `src/main.rs`:
23
+ the CLI, thin; it is the root package so that pre-commit's
24
+ `cargo install --path .` works.
25
+ - `crates/mathfmt-core/data/`: the tables. `unicode-math.tsv`,
26
+ `alphabets.tsv` and `packages.tsv` are generated; `latex.tsv` and
27
+ `signatures.tsv` are written by hand and are where a symbol, an alias or a
28
+ command signature is added.
29
+ - `tools/gen-table.py`: regenerates the two Unicode tables from
30
+ `kpsewhich unicode-math-table.tex`. `tools/gen-packages.py`: regenerates
31
+ `packages.tsv` by asking pdflatex and lualatex which package defines each
32
+ command the tables emit; run it after adding a command to `latex.tsv` or a
33
+ style command to `signatures.tsv` (a unit test fails until it is).
34
+ - `tests/fixtures/<case>/`: the golden cases, run by `tests/golden.rs` and
35
+ compiled and compared by `tests/verify.rs`; `tests/cli.rs`: the binary's
36
+ interface. `tests/projects/<case>/`: small document trees with a
37
+ `mathfmt.toml` naming their `root`, converted and compared with
38
+ `expected/` by `tests/project.rs` (and `expected.declarations`), and
39
+ compiled by `tests/verify.rs` with `--project --verify --declare`.
40
+ `tests/dev.sh`: the dev shell wrapper. `flake.nix`: dev shell,
41
+ package and checks. `src/verify.rs`: the `--verify` mode (compile both
42
+ sides, rasterize, compare).
43
+ - `pyproject.toml`: the Python distribution `mathfmt-tex`, a wheel holding
44
+ the binary only (maturin, `bindings = "bin"`); `.github/workflows/wheels.yml`
45
+ builds it per platform on the GitHub mirror and publishes from a `v*` tag;
46
+ `tests/pre-commit-e2e.sh` runs it as a pre-commit hook by hand.
47
+ - `.phalanix/`: the rig (lead, and task/review/question templates) and its
48
+ roles, for waves run on phalanix.
49
+
50
+ ## Building and testing
51
+
52
+ On a laptop with network, plain `cargo test --workspace` works. Everywhere
53
+ else, and always in a seat, go through the wrapper, which enters the dev shell
54
+ from the repository root and runs cargo offline against the crates nix vendored
55
+ from `Cargo.lock`:
56
+
57
+ ```sh
58
+ tests/dev.sh cargo test --workspace
59
+ tests/dev.sh cargo clippy --workspace --all-targets -- -D warnings
60
+ tests/dev.sh cargo fmt --check
61
+ repo/tests/dev.sh cargo test --workspace # from a seat's state directory
62
+ ```
63
+
64
+ Never use a bare `nix develop` in a seat: the wrapper drops every `PHALANIX_*`
65
+ and `PG*` variable first (a phalanix binary that sees them dials the live bus
66
+ as the seat), puts `nix` on PATH (a seat's PATH has none) and runs from the
67
+ root. If a seat's PATH has no bash for the shebang, run
68
+ `/run/current-system/sw/bin/bash repo/tests/dev.sh <command>` and report it.
69
+
70
+ - The dev shell writes `.cargo-home/config.toml` (git-ignored), pointing
71
+ cargo at the vendored crates and setting it offline, so `cargo`,
72
+ `cargo clippy` and rust-analyzer all build without network.
73
+ - Nix sees only what git tracks. Commit, or at least `git add`, before any
74
+ `nix build`, `nix flake check` or a wrapper run that needs a new file; a new
75
+ dependency reaches the shell once the updated `Cargo.lock` is added.
76
+ - `nix build` builds the binary and runs the tests. The flake checks are
77
+ `build`, `clippy`, `fmt`, `verify` and `wheel` (the Python wheel builds
78
+ offline and its `mathfmt` runs): `nix build .#checks.x86_64-linux.<name>`,
79
+ or in a seat `/run/current-system/sw/bin/nix build ./repo#checks.x86_64-linux.<name>`.
80
+ `verify` brings a TeX Live and runs `tests/verify.rs` with
81
+ `MATHFMT_VERIFY_REQUIRED` set; elsewhere that test skips itself when
82
+ `pdflatex` or `pdftoppm` is missing (the dev shell on a machine with TeX
83
+ Live has them on PATH, so `cargo test` runs it).
84
+ - Dependencies are what `Cargo.toml` lists; a new one is a decision for the
85
+ plan, not a convenience.
86
+
87
+ A wave is verified when the tests, clippy, fmt and every flake check pass on its
88
+ last commit, and the ledger records the counts.
89
+
90
+ ## Fixtures
91
+
92
+ A golden case is a directory `tests/fixtures/<case>/` holding `input.tex`,
93
+ `expected.tex`, and optionally `mathfmt.toml` (the config; the defaults
94
+ otherwise), `expected.stderr` (the diagnostics) and `args` (one CLI argument
95
+ per line, from the subset the harness understands: `--profile NAME`,
96
+ `--macros FILE`, `rename --from X --to Y`). The harness runs every case,
97
+ compares bytes, then asserts idempotence: `expected.tex` through the same
98
+ profile is unchanged. Cases tagged for round
99
+ trips are run unicode to ascii to unicode and must return to the start.
100
+
101
+ Every case is also compiled before and after (`tests/verify.rs` runs
102
+ `mathfmt --verify` on `input.tex` with the case's `args`) and must render the
103
+ same pages. A case whose conversion changes the typeset result on purpose (a
104
+ rename; a spelling that repairs pdflatex) says so in `expected.verify`:
105
+ `differs: reason`. One that cannot compile at all says `skip: reason`. A
106
+ snippet must be compilable: define the macros it uses, and keep preamble-only
107
+ commands (`\usepackage`, `\DeclareMathOperator`) on their own lines, which the
108
+ wrapper hoists.
109
+
110
+ - Test first: the fixture or unit test that pins the behaviour, watched to fail,
111
+ then the code.
112
+ - Every bug becomes a fixture named for what it pins.
113
+ - Never weaken a test or hand-edit an `expected.tex` to make a run pass. An
114
+ expected file is read, not copied from the output.
115
+
116
+ ## Tables
117
+
118
+ - A symbol, an alias or a preferred ASCII spelling is a row in `latex.tsv`; a
119
+ command's argument count or text-argument flag is a row in `signatures.tsv`.
120
+ Not a match arm.
121
+ - `unicode-math.tsv` and `alphabets.tsv` are regenerated only by
122
+ `tools/gen-table.py`, when TeX Live updates; review the diff before adding it.
123
+ - `mathfmt table` prints what the binary believes; trust it over memory.
124
+
125
+ ## Rules
126
+
127
+ These are the person's standing rules. They bind every seat and every session.
128
+
129
+ - **Commits.** `area: what changed`, lowercase, as `git log` shows, a title of
130
+ 60 characters or fewer and a body that says why. Never put a model identifier
131
+ (opus, sonnet, haiku, fable, mythos or any other) in a commit's title or
132
+ body. The harness's own `Co-Authored-By` trailer is the exception. Never skip
133
+ hooks and never change git identity.
134
+ - **Leave alone what is not yours.** Don't touch another session's uncommitted
135
+ work, and don't stop processes you did not start.
136
+ - **Refusals.** When the permission system refuses a step, report it. Never
137
+ route around it, and never ask another seat to do it for you.
138
+ - **Releases.** The person lands, pushes, tags and releases. Nothing reaches
139
+ `main` any other way.
140
+ - **Review as you go.** Every wave gets an independent adversarial review
141
+ before it lands; each finding is fixed or ruled on before the next wave.
142
+ - **The spec matches what ships.** When behaviour departs from the design,
143
+ amend the spec in the same change.