parla 0.0.1__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 (216) hide show
  1. parla-0.0.1/LICENSE +13 -0
  2. parla-0.0.1/MANIFEST.in +2 -0
  3. parla-0.0.1/PKG-INFO +94 -0
  4. parla-0.0.1/README.md +71 -0
  5. parla-0.0.1/pyproject.toml +54 -0
  6. parla-0.0.1/setup.cfg +4 -0
  7. parla-0.0.1/src/parla/__init__.py +3 -0
  8. parla-0.0.1/src/parla/assets/fonts/LICENSE-Latin-Modern.txt +171 -0
  9. parla-0.0.1/src/parla/assets/fonts/lmmono10-italic.otf +0 -0
  10. parla-0.0.1/src/parla/assets/fonts/lmmono10-regular.otf +0 -0
  11. parla-0.0.1/src/parla/assets/fonts/lmmonolt10-bold.otf +0 -0
  12. parla-0.0.1/src/parla/assets/fonts/lmsans10-bold.otf +0 -0
  13. parla-0.0.1/src/parla/assets/fonts/lmsans10-boldoblique.otf +0 -0
  14. parla-0.0.1/src/parla/assets/fonts/lmsans10-oblique.otf +0 -0
  15. parla-0.0.1/src/parla/assets/fonts/lmsans10-regular.otf +0 -0
  16. parla-0.0.1/src/parla/casebook.py +180 -0
  17. parla-0.0.1/src/parla/checks/__init__.py +3 -0
  18. parla-0.0.1/src/parla/checks/finding.py +28 -0
  19. parla-0.0.1/src/parla/checks/ids.py +59 -0
  20. parla-0.0.1/src/parla/checks/shared.py +57 -0
  21. parla-0.0.1/src/parla/cli.py +559 -0
  22. parla-0.0.1/src/parla/config.py +371 -0
  23. parla-0.0.1/src/parla/constraints.py +432 -0
  24. parla-0.0.1/src/parla/data/config.example.toml +32 -0
  25. parla-0.0.1/src/parla/docker/Dockerfile +49 -0
  26. parla-0.0.1/src/parla/docker/tl-packages.txt +11 -0
  27. parla-0.0.1/src/parla/docker/tl.profile +14 -0
  28. parla-0.0.1/src/parla/engines/__init__.py +0 -0
  29. parla-0.0.1/src/parla/engines/base.py +97 -0
  30. parla-0.0.1/src/parla/engines/latex/__init__.py +0 -0
  31. parla-0.0.1/src/parla/engines/latex/autoload.py +62 -0
  32. parla-0.0.1/src/parla/engines/latex/bib.py +348 -0
  33. parla-0.0.1/src/parla/engines/latex/build.py +269 -0
  34. parla-0.0.1/src/parla/engines/latex/checks.py +446 -0
  35. parla-0.0.1/src/parla/engines/latex/diff.py +100 -0
  36. parla-0.0.1/src/parla/engines/latex/engine.py +82 -0
  37. parla-0.0.1/src/parla/engines/latex/export.py +298 -0
  38. parla-0.0.1/src/parla/engines/latex/image.py +90 -0
  39. parla-0.0.1/src/parla/engines/latex/links.py +36 -0
  40. parla-0.0.1/src/parla/engines/none/__init__.py +0 -0
  41. parla-0.0.1/src/parla/engines/none/engine.py +36 -0
  42. parla-0.0.1/src/parla/engines/registry.py +22 -0
  43. parla-0.0.1/src/parla/engines/svg/__init__.py +0 -0
  44. parla-0.0.1/src/parla/engines/svg/build.py +102 -0
  45. parla-0.0.1/src/parla/engines/svg/checks.py +460 -0
  46. parla-0.0.1/src/parla/engines/svg/engine.py +56 -0
  47. parla-0.0.1/src/parla/engines/svg/fonts.py +58 -0
  48. parla-0.0.1/src/parla/frontmatter.py +16 -0
  49. parla-0.0.1/src/parla/guides/__init__.py +3 -0
  50. parla-0.0.1/src/parla/guides/audience/default.md +7 -0
  51. parla-0.0.1/src/parla/guides/constraints.jsonl +6 -0
  52. parla-0.0.1/src/parla/guides/edit.py +191 -0
  53. parla-0.0.1/src/parla/guides/engine/beamer.md +8 -0
  54. parla-0.0.1/src/parla/guides/engine/cv.md +12 -0
  55. parla-0.0.1/src/parla/guides/engine/latex.md +11 -0
  56. parla-0.0.1/src/parla/guides/engine/none.md +7 -0
  57. parla-0.0.1/src/parla/guides/engine/svg.md +10 -0
  58. parla-0.0.1/src/parla/guides/general.md +4 -0
  59. parla-0.0.1/src/parla/guides/lint.py +115 -0
  60. parla-0.0.1/src/parla/guides/modality/cv.md +24 -0
  61. parla-0.0.1/src/parla/guides/modality/diagram.md +18 -0
  62. parla-0.0.1/src/parla/guides/modality/paper.md +23 -0
  63. parla-0.0.1/src/parla/guides/modality/short-paper.md +9 -0
  64. parla-0.0.1/src/parla/guides/modality/social-post.md +18 -0
  65. parla-0.0.1/src/parla/guides/modality/talk.md +24 -0
  66. parla-0.0.1/src/parla/guides/modality/thesis.md +11 -0
  67. parla-0.0.1/src/parla/guides/move/compress.md +15 -0
  68. parla-0.0.1/src/parla/guides/move/elaborate.md +7 -0
  69. parla-0.0.1/src/parla/guides/move/hedge.md +9 -0
  70. parla-0.0.1/src/parla/guides/move/sharpen.md +9 -0
  71. parla-0.0.1/src/parla/guides/pages.py +151 -0
  72. parla-0.0.1/src/parla/guides/procedure/condense-table.md +7 -0
  73. parla-0.0.1/src/parla/guides/procedure/edit.md +7 -0
  74. parla-0.0.1/src/parla/guides/procedure/fit-pages.md +7 -0
  75. parla-0.0.1/src/parla/guides/procedure/ground.md +7 -0
  76. parla-0.0.1/src/parla/guides/procedure/read-figure.md +7 -0
  77. parla-0.0.1/src/parla/guides/procedure/read-paper.md +7 -0
  78. parla-0.0.1/src/parla/guides/rules.py +129 -0
  79. parla-0.0.1/src/parla/guides/show.py +391 -0
  80. parla-0.0.1/src/parla/guides/topic/abstract.md +10 -0
  81. parla-0.0.1/src/parla/guides/topic/bullets.md +6 -0
  82. parla-0.0.1/src/parla/guides/topic/captions.md +6 -0
  83. parla-0.0.1/src/parla/guides/topic/citations.md +6 -0
  84. parla-0.0.1/src/parla/guides/topic/claims.md +6 -0
  85. parla-0.0.1/src/parla/guides/topic/contributions.md +6 -0
  86. parla-0.0.1/src/parla/guides/topic/deck.md +8 -0
  87. parla-0.0.1/src/parla/guides/topic/diagram.md +8 -0
  88. parla-0.0.1/src/parla/guides/topic/figures.md +6 -0
  89. parla-0.0.1/src/parla/guides/topic/flow.md +6 -0
  90. parla-0.0.1/src/parla/guides/topic/selection.md +6 -0
  91. parla-0.0.1/src/parla/guides/topic/skills.md +8 -0
  92. parla-0.0.1/src/parla/guides/topic/summary.md +6 -0
  93. parla-0.0.1/src/parla/guides/topic/tables.md +6 -0
  94. parla-0.0.1/src/parla/guides/topic/terminology.md +6 -0
  95. parla-0.0.1/src/parla/guides/trait/narrative/engineering.md +5 -0
  96. parla-0.0.1/src/parla/guides/trait/narrative/executive.md +5 -0
  97. parla-0.0.1/src/parla/guides/trait/narrative/scientific.md +5 -0
  98. parla-0.0.1/src/parla/guides/trait/narrative/teaching.md +5 -0
  99. parla-0.0.1/src/parla/guides/trait/pacing/balanced.md +5 -0
  100. parla-0.0.1/src/parla/guides/trait/pacing/generous.md +5 -0
  101. parla-0.0.1/src/parla/guides/trait/pacing/tight.md +5 -0
  102. parla-0.0.1/src/parla/guides/trait/reader/cross-domain.md +5 -0
  103. parla-0.0.1/src/parla/guides/trait/reader/newcomer.md +5 -0
  104. parla-0.0.1/src/parla/guides/trait/reader/peer.md +5 -0
  105. parla-0.0.1/src/parla/guides/trait/reader/screener.md +5 -0
  106. parla-0.0.1/src/parla/guides/trait/visual/academic.md +5 -0
  107. parla-0.0.1/src/parla/guides/trait/visual/keynote.md +5 -0
  108. parla-0.0.1/src/parla/jobs.py +170 -0
  109. parla-0.0.1/src/parla/names.py +25 -0
  110. parla-0.0.1/src/parla/py.typed +0 -0
  111. parla-0.0.1/src/parla/router.py +237 -0
  112. parla-0.0.1/src/parla/settings.py +93 -0
  113. parla-0.0.1/src/parla/skills/parla/SKILL.md +108 -0
  114. parla-0.0.1/src/parla/skills/parla/artifacts.md +25 -0
  115. parla-0.0.1/src/parla/skills/parla/asking.md +27 -0
  116. parla-0.0.1/src/parla/skills/parla/constraints.md +25 -0
  117. parla-0.0.1/src/parla/skills/parla/distilling.md +40 -0
  118. parla-0.0.1/src/parla/skills/parla/jobs.md +37 -0
  119. parla-0.0.1/src/parla/skills/parla/learning.md +59 -0
  120. parla-0.0.1/src/parla/skills/parla/moves.md +17 -0
  121. parla-0.0.1/src/parla/skills/parla/relocating.md +29 -0
  122. parla-0.0.1/src/parla/skills/parla/seeding.md +9 -0
  123. parla-0.0.1/src/parla/skills/parla/suite.md +38 -0
  124. parla-0.0.1/src/parla/skills/parla/worker.md +30 -0
  125. parla-0.0.1/src/parla/template.py +275 -0
  126. parla-0.0.1/src/parla/texmf/bibtex/bib/arxiv-references.bib +27 -0
  127. parla-0.0.1/src/parla/texmf/tex/latex/arxiv-style/License.txt +21 -0
  128. parla-0.0.1/src/parla/texmf/tex/latex/arxiv-style/README.md +49 -0
  129. parla-0.0.1/src/parla/texmf/tex/latex/arxiv-style/arxiv.sty +262 -0
  130. parla-0.0.1/src/parla/texmf/tex/latex/arxiv-style/orcid.pdf +0 -0
  131. parla-0.0.1/src/parla/texmf/tex/latex/arxiv-style/template.pdf +0 -0
  132. parla-0.0.1/src/parla/texmf/tex/latex/arxiv-style/template.tex +214 -0
  133. parla-0.0.1/src/parla/tools/__init__.py +0 -0
  134. parla-0.0.1/src/parla/tools/claims.py +124 -0
  135. parla-0.0.1/src/parla/tools/formula.py +84 -0
  136. parla-0.0.1/src/parla/tools/measure.py +72 -0
  137. parla-0.0.1/src/parla/tools/pdf/__init__.py +39 -0
  138. parla-0.0.1/src/parla/tools/pdf/base.py +28 -0
  139. parla-0.0.1/src/parla/tools/pdf/heuristics.py +529 -0
  140. parla-0.0.1/src/parla/tools/pdf/pdftotext.py +131 -0
  141. parla-0.0.1/src/parla/tools/pdf/pymupdf.py +127 -0
  142. parla-0.0.1/src/parla/tools/pdf/pymupdf4llm.py +112 -0
  143. parla-0.0.1/src/parla/tools/sections.py +909 -0
  144. parla-0.0.1/src/parla/tools/source.py +109 -0
  145. parla-0.0.1/src/parla/tools/table.py +64 -0
  146. parla-0.0.1/src/parla/tools/table_fit.py +181 -0
  147. parla-0.0.1/src/parla/tools/table_parse.py +357 -0
  148. parla-0.0.1/src/parla/tools/table_render.py +191 -0
  149. parla-0.0.1/src/parla.egg-info/PKG-INFO +94 -0
  150. parla-0.0.1/src/parla.egg-info/SOURCES.txt +214 -0
  151. parla-0.0.1/src/parla.egg-info/dependency_links.txt +1 -0
  152. parla-0.0.1/src/parla.egg-info/entry_points.txt +2 -0
  153. parla-0.0.1/src/parla.egg-info/requires.txt +14 -0
  154. parla-0.0.1/src/parla.egg-info/top_level.txt +1 -0
  155. parla-0.0.1/tests/conftest.py +42 -0
  156. parla-0.0.1/tests/fixtures/guides/private/audience/ai-rd.md +8 -0
  157. parla-0.0.1/tests/fixtures/guides/private/audience/icra.md +8 -0
  158. parla-0.0.1/tests/fixtures/guides/private/audience/linkedin.md +8 -0
  159. parla-0.0.1/tests/fixtures/guides/private/engine/beamer.md +8 -0
  160. parla-0.0.1/tests/fixtures/guides/private/engine/latex.md +8 -0
  161. parla-0.0.1/tests/fixtures/guides/private/engine/none.md +8 -0
  162. parla-0.0.1/tests/fixtures/guides/private/engine/svg.md +8 -0
  163. parla-0.0.1/tests/fixtures/guides/private/general.md +7 -0
  164. parla-0.0.1/tests/fixtures/guides/private/modality/cv.md +8 -0
  165. parla-0.0.1/tests/fixtures/guides/private/modality/paper.md +8 -0
  166. parla-0.0.1/tests/fixtures/guides/private/modality/social-post.md +8 -0
  167. parla-0.0.1/tests/fixtures/guides/private/topic/contributions.md +9 -0
  168. parla-0.0.1/tests/fixtures/guides/private/topic/evidence.md +9 -0
  169. parla-0.0.1/tests/fixtures/guides/private/topic/positioning.md +25 -0
  170. parla-0.0.1/tests/fixtures/guides/private/trait/reader/cross-domain.md +8 -0
  171. parla-0.0.1/tests/fixtures/guides/private/trait/reader/peer.md +8 -0
  172. parla-0.0.1/tests/fixtures/guides/public/audience/ai-rd.md +13 -0
  173. parla-0.0.1/tests/fixtures/guides/public/audience/default.md +7 -0
  174. parla-0.0.1/tests/fixtures/guides/public/audience/icra.md +16 -0
  175. parla-0.0.1/tests/fixtures/guides/public/audience/linkedin.md +8 -0
  176. parla-0.0.1/tests/fixtures/guides/public/constraints.jsonl +1 -0
  177. parla-0.0.1/tests/fixtures/guides/public/engine/beamer.md +7 -0
  178. parla-0.0.1/tests/fixtures/guides/public/engine/latex.md +7 -0
  179. parla-0.0.1/tests/fixtures/guides/public/engine/none.md +7 -0
  180. parla-0.0.1/tests/fixtures/guides/public/engine/svg.md +7 -0
  181. parla-0.0.1/tests/fixtures/guides/public/general.md +4 -0
  182. parla-0.0.1/tests/fixtures/guides/public/modality/cv.md +14 -0
  183. parla-0.0.1/tests/fixtures/guides/public/modality/diagram.md +14 -0
  184. parla-0.0.1/tests/fixtures/guides/public/modality/paper.md +17 -0
  185. parla-0.0.1/tests/fixtures/guides/public/modality/social-post.md +14 -0
  186. parla-0.0.1/tests/fixtures/guides/public/modality/talk.md +17 -0
  187. parla-0.0.1/tests/fixtures/guides/public/topic/positioning.md +6 -0
  188. parla-0.0.1/tests/fixtures/guides/public/topic/slides-only.md +8 -0
  189. parla-0.0.1/tests/fixtures/guides/public/trait/reader/cross-domain.md +5 -0
  190. parla-0.0.1/tests/fixtures/guides/public/trait/reader/peer.md +5 -0
  191. parla-0.0.1/tests/fixtures/svg/knn-graph.svg +92 -0
  192. parla-0.0.1/tests/test_autoload.py +61 -0
  193. parla-0.0.1/tests/test_bib.py +170 -0
  194. parla-0.0.1/tests/test_build.py +219 -0
  195. parla-0.0.1/tests/test_checks_latex.py +249 -0
  196. parla-0.0.1/tests/test_cli.py +240 -0
  197. parla-0.0.1/tests/test_constraints.py +288 -0
  198. parla-0.0.1/tests/test_conventions.py +65 -0
  199. parla-0.0.1/tests/test_diff.py +114 -0
  200. parla-0.0.1/tests/test_docker.py +68 -0
  201. parla-0.0.1/tests/test_engines.py +286 -0
  202. parla-0.0.1/tests/test_export.py +299 -0
  203. parla-0.0.1/tests/test_guides.py +687 -0
  204. parla-0.0.1/tests/test_image.py +41 -0
  205. parla-0.0.1/tests/test_jobs.py +169 -0
  206. parla-0.0.1/tests/test_links.py +103 -0
  207. parla-0.0.1/tests/test_packaging.py +42 -0
  208. parla-0.0.1/tests/test_settings.py +73 -0
  209. parla-0.0.1/tests/test_skill.py +32 -0
  210. parla-0.0.1/tests/test_smoke.py +2 -0
  211. parla-0.0.1/tests/test_svg_engine.py +211 -0
  212. parla-0.0.1/tests/test_table_fit_and_render.py +295 -0
  213. parla-0.0.1/tests/test_table_parse.py +151 -0
  214. parla-0.0.1/tests/test_template_init.py +189 -0
  215. parla-0.0.1/tests/test_tools.py +260 -0
  216. parla-0.0.1/tests/test_translations.py +72 -0
parla-0.0.1/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2026 ptonso
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
@@ -0,0 +1,2 @@
1
+ graft tests
2
+ global-exclude __pycache__ *.py[cod]
parla-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.4
2
+ Name: parla
3
+ Version: 0.0.1
4
+ Summary: Agent-first tool for writing papers, slides, posts and diagrams in their final format, guided by the author's guides and corrections.
5
+ Author: ptonso
6
+ License-Expression: Apache-2.0
7
+ Keywords: latex,agent,writing,docker,pdf
8
+ Requires-Python: >=3.12
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: PyYAML>=6.0
12
+ Provides-Extra: pdf
13
+ Requires-Dist: pymupdf>=1.24.0; extra == "pdf"
14
+ Requires-Dist: pymupdf4llm>=0.3.0; extra == "pdf"
15
+ Provides-Extra: svg
16
+ Requires-Dist: fonttools>=4.40; extra == "svg"
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=7.4; extra == "dev"
19
+ Requires-Dist: ruff>=0.6; extra == "dev"
20
+ Requires-Dist: mypy>=1.7; extra == "dev"
21
+ Requires-Dist: types-PyYAML; extra == "dev"
22
+ Dynamic: license-file
23
+
24
+ # parla
25
+
26
+ `parla` lets a coding agent write papers, CVs, talks, posts and diagrams in your own voice.
27
+
28
+ It keeps your long-term memory of how you write, loads the part of it that the section being written needs, checks the text against the rules of yours that a tool can test, and asks you before anything becomes permanent. The goal is to automate communication while keeping your idiosyncrasies.
29
+
30
+ `parla` works as a plugin for agentic tools. It ships a skill that Claude Code and opencode load, and the skill carries the working method: how a job is routed, when the agent asks you, and how it learns from your corrections. The `parla` CLI supplies what the agent cannot do alone, and you talk to the agent as usual:
31
+
32
+ ```
33
+ /parla icra-paper introduction
34
+ ```
35
+
36
+ - **Final format.** The agent edits LaTeX, SVG or plain text directly, and `parla` builds it in a pinned Docker image. See the [architecture](https://github.com/ptonso/parla/blob/master/specs/architecture.md).
37
+ - **Good defaults.** A brief per modality, a note per topic and a few starter checks give a reasonable first draft before you teach anything. See [guides](https://github.com/ptonso/parla/blob/master/specs/guides.md).
38
+ - **Checks what can be tested.** A pattern, such as an em dash or a reference form, fails the build with its line, and a question, such as whether a citation supports its claim, is located for the agent to answer with a quoted line. See [constraints](https://github.com/ptonso/parla/blob/master/specs/constraints.md).
39
+ - **Learns from corrections.** Your fixes become cases, rules or constraints, always with your approval. See the [casebook](https://github.com/ptonso/parla/blob/master/specs/casebook.md).
40
+ - **Safe to steer.** Every job is snapshotted and diffed, and can be reverted. See [jobs](https://github.com/ptonso/parla/blob/master/specs/jobs.md).
41
+ - **Local.** Your guides and casebook are plain files on your machine, and `parla` makes no model calls.
42
+
43
+ ## Vision
44
+
45
+ `parla` keeps a human in the loop. You make the decisions about what to say and which writing preferences to keep. The agent applies the decisions you already made and proposes text that follows them. It asks before anything becomes permanent: a case, a rule, a constraint or a claim in the card.
46
+
47
+ A job has two roles. The agent you talk to chooses the topics the section needs (`parla guide list topic` shows each with its size and when it applies), relays the worker's questions to you and keeps your memories. One worker subagent writes. Its prompt is short, about 1000 words: your rules for those topics, one-line hooks of earlier corrections and the card ([skill spec](https://github.com/ptonso/parla/blob/master/specs/skill.md)). The worker is done when `parla check` ends with `suite: 0 fail, 0 probes open`.
48
+
49
+ ## How it learns
50
+
51
+ When a job ends, the agent proposes where each correction belongs and you accept, rephrase or decline: a case for an illustration, a rule for a matter of degree, a constraint for what a pattern or a yes-or-no question can decide. When several cases share a lesson, it proposes a rule with the cases as evidence, and it keeps the guides inside their size budget by merging and rewriting rules.
52
+
53
+ | Memory | Holds | Changes |
54
+ |---|---|---|
55
+ | Guides | your stable rules, on top of the shipped defaults | slowly |
56
+ | Constraints | patterns and questions with an exact answer, run by every check and build | when a correction has an exact answer |
57
+ | Casebook | your corrections, each with a before, an after and a lesson | every job |
58
+ | Card | the decisions of one artifact: claim strength, scope, which result leads | per artifact |
59
+
60
+ More in [how it learns](https://github.com/ptonso/parla/blob/master/docs/guide/learning.md).
61
+
62
+ ## Modalities
63
+
64
+ | Modality | Output |
65
+ |---|---|
66
+ | `paper`, `short-paper`, `thesis` | PDF, plus a change PDF against the job snapshot |
67
+ | `cv` | PDF |
68
+ | `talk` | Beamer PDF |
69
+ | `diagram` | SVG and PNG |
70
+ | `social-post` | plain text |
71
+
72
+ ## Quick start
73
+
74
+ `parla` needs Python 3.12 and Docker.
75
+
76
+ ```bash
77
+ pip install "parla[svg,pdf]"
78
+ parla image build
79
+ parla init
80
+ parla init artifact my-paper --modality paper
81
+ ```
82
+
83
+ `parla init` links the skill into `.claude/skills` and `.agents/skills`. Open your agent and type `/parla my-paper`. The full walk-through is in the [quick start](https://github.com/ptonso/parla/blob/master/docs/guide/quickstart.md).
84
+
85
+ ## Documentation
86
+
87
+ - [Quick start](https://github.com/ptonso/parla/blob/master/docs/guide/quickstart.md): install, first artifact, first job.
88
+ - [How it learns](https://github.com/ptonso/parla/blob/master/docs/guide/learning.md): cases, rules, constraints and the four memories.
89
+ - [Development](https://github.com/ptonso/parla/blob/master/docs/guide/development.md): tests and linting.
90
+ - [specs/](https://github.com/ptonso/parla/blob/master/specs/README.md): the contract of every command and format. The agent's method is the [skill spec](https://github.com/ptonso/parla/blob/master/specs/skill.md).
91
+
92
+ ## License
93
+
94
+ Apache 2.0. See [LICENSE](https://github.com/ptonso/parla/blob/master/LICENSE) and [THIRD-PARTY.md](https://github.com/ptonso/parla/blob/master/THIRD-PARTY.md).
parla-0.0.1/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # parla
2
+
3
+ `parla` lets a coding agent write papers, CVs, talks, posts and diagrams in your own voice.
4
+
5
+ It keeps your long-term memory of how you write, loads the part of it that the section being written needs, checks the text against the rules of yours that a tool can test, and asks you before anything becomes permanent. The goal is to automate communication while keeping your idiosyncrasies.
6
+
7
+ `parla` works as a plugin for agentic tools. It ships a skill that Claude Code and opencode load, and the skill carries the working method: how a job is routed, when the agent asks you, and how it learns from your corrections. The `parla` CLI supplies what the agent cannot do alone, and you talk to the agent as usual:
8
+
9
+ ```
10
+ /parla icra-paper introduction
11
+ ```
12
+
13
+ - **Final format.** The agent edits LaTeX, SVG or plain text directly, and `parla` builds it in a pinned Docker image. See the [architecture](https://github.com/ptonso/parla/blob/master/specs/architecture.md).
14
+ - **Good defaults.** A brief per modality, a note per topic and a few starter checks give a reasonable first draft before you teach anything. See [guides](https://github.com/ptonso/parla/blob/master/specs/guides.md).
15
+ - **Checks what can be tested.** A pattern, such as an em dash or a reference form, fails the build with its line, and a question, such as whether a citation supports its claim, is located for the agent to answer with a quoted line. See [constraints](https://github.com/ptonso/parla/blob/master/specs/constraints.md).
16
+ - **Learns from corrections.** Your fixes become cases, rules or constraints, always with your approval. See the [casebook](https://github.com/ptonso/parla/blob/master/specs/casebook.md).
17
+ - **Safe to steer.** Every job is snapshotted and diffed, and can be reverted. See [jobs](https://github.com/ptonso/parla/blob/master/specs/jobs.md).
18
+ - **Local.** Your guides and casebook are plain files on your machine, and `parla` makes no model calls.
19
+
20
+ ## Vision
21
+
22
+ `parla` keeps a human in the loop. You make the decisions about what to say and which writing preferences to keep. The agent applies the decisions you already made and proposes text that follows them. It asks before anything becomes permanent: a case, a rule, a constraint or a claim in the card.
23
+
24
+ A job has two roles. The agent you talk to chooses the topics the section needs (`parla guide list topic` shows each with its size and when it applies), relays the worker's questions to you and keeps your memories. One worker subagent writes. Its prompt is short, about 1000 words: your rules for those topics, one-line hooks of earlier corrections and the card ([skill spec](https://github.com/ptonso/parla/blob/master/specs/skill.md)). The worker is done when `parla check` ends with `suite: 0 fail, 0 probes open`.
25
+
26
+ ## How it learns
27
+
28
+ When a job ends, the agent proposes where each correction belongs and you accept, rephrase or decline: a case for an illustration, a rule for a matter of degree, a constraint for what a pattern or a yes-or-no question can decide. When several cases share a lesson, it proposes a rule with the cases as evidence, and it keeps the guides inside their size budget by merging and rewriting rules.
29
+
30
+ | Memory | Holds | Changes |
31
+ |---|---|---|
32
+ | Guides | your stable rules, on top of the shipped defaults | slowly |
33
+ | Constraints | patterns and questions with an exact answer, run by every check and build | when a correction has an exact answer |
34
+ | Casebook | your corrections, each with a before, an after and a lesson | every job |
35
+ | Card | the decisions of one artifact: claim strength, scope, which result leads | per artifact |
36
+
37
+ More in [how it learns](https://github.com/ptonso/parla/blob/master/docs/guide/learning.md).
38
+
39
+ ## Modalities
40
+
41
+ | Modality | Output |
42
+ |---|---|
43
+ | `paper`, `short-paper`, `thesis` | PDF, plus a change PDF against the job snapshot |
44
+ | `cv` | PDF |
45
+ | `talk` | Beamer PDF |
46
+ | `diagram` | SVG and PNG |
47
+ | `social-post` | plain text |
48
+
49
+ ## Quick start
50
+
51
+ `parla` needs Python 3.12 and Docker.
52
+
53
+ ```bash
54
+ pip install "parla[svg,pdf]"
55
+ parla image build
56
+ parla init
57
+ parla init artifact my-paper --modality paper
58
+ ```
59
+
60
+ `parla init` links the skill into `.claude/skills` and `.agents/skills`. Open your agent and type `/parla my-paper`. The full walk-through is in the [quick start](https://github.com/ptonso/parla/blob/master/docs/guide/quickstart.md).
61
+
62
+ ## Documentation
63
+
64
+ - [Quick start](https://github.com/ptonso/parla/blob/master/docs/guide/quickstart.md): install, first artifact, first job.
65
+ - [How it learns](https://github.com/ptonso/parla/blob/master/docs/guide/learning.md): cases, rules, constraints and the four memories.
66
+ - [Development](https://github.com/ptonso/parla/blob/master/docs/guide/development.md): tests and linting.
67
+ - [specs/](https://github.com/ptonso/parla/blob/master/specs/README.md): the contract of every command and format. The agent's method is the [skill spec](https://github.com/ptonso/parla/blob/master/specs/skill.md).
68
+
69
+ ## License
70
+
71
+ Apache 2.0. See [LICENSE](https://github.com/ptonso/parla/blob/master/LICENSE) and [THIRD-PARTY.md](https://github.com/ptonso/parla/blob/master/THIRD-PARTY.md).
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "parla"
7
+ version = "0.0.1"
8
+ description = "Agent-first tool for writing papers, slides, posts and diagrams in their final format, guided by the author's guides and corrections."
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = "Apache-2.0"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "ptonso" }]
14
+ keywords = ["latex", "agent", "writing", "docker", "pdf"]
15
+ dependencies = ["PyYAML>=6.0"]
16
+
17
+ [project.scripts]
18
+ parla = "parla.cli:main"
19
+
20
+ [project.optional-dependencies]
21
+ pdf = ["pymupdf>=1.24.0", "pymupdf4llm>=0.3.0"]
22
+ svg = ["fonttools>=4.40"]
23
+ dev = ["pytest>=7.4", "ruff>=0.6", "mypy>=1.7", "types-PyYAML"]
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
27
+
28
+ [tool.setuptools.package-data]
29
+ parla = ["docker/*", "data/*", "texmf/**/*", "assets/fonts/*", "guides/**/*.md", "guides/*.jsonl", "skills/parla/*.md", "py.typed"]
30
+
31
+ [tool.pytest.ini_options]
32
+ minversion = "7.0"
33
+ addopts = "-q"
34
+ markers = ["docker: needs a running Docker daemon"]
35
+ pythonpath = ["src"]
36
+
37
+ [tool.ruff]
38
+ line-length = 100
39
+
40
+ [tool.ruff.lint]
41
+ select = ["E", "F"]
42
+ ignore = ["E501"]
43
+
44
+ [tool.mypy]
45
+ python_version = "3.12"
46
+
47
+ # Copied from texfy-ai with its typing; the copies are compared against the originals, not retyped.
48
+ [[tool.mypy.overrides]]
49
+ module = ["parla.tools.pdf.*", "parla.tools.sections", "parla.tools.table_parse", "parla.tools.table_fit", "parla.tools.table_render"]
50
+ ignore_errors = true
51
+
52
+ [[tool.mypy.overrides]]
53
+ module = ["fontTools.*"]
54
+ ignore_missing_imports = true
parla-0.0.1/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """parla."""
2
+
3
+ __version__ = "0.0.1"
@@ -0,0 +1,171 @@
1
+ This package was debianized by Florent Rougon <f.rougon@free.fr> on
2
+ Tue, 27 Jan 2004 20:12:21 +0100.
3
+
4
+ The following TeX Live packages were downloaded from
5
+ http://www.ctan.org/tex-archive/systems/texlive/tlnet/archive/
6
+ and merged into one orig.tar.gz file:
7
+ lm.tar.xz
8
+ lm.doc.tar.xz
9
+ lm-math.tar.xz
10
+ lm-math.doc.tar.xz
11
+
12
+
13
+ Upstream work
14
+ -------------
15
+
16
+ The upstream README-Latin-Modern.txt file says:
17
+
18
+ Font: The Latin Modern Family of Fonts
19
+ Designer (Computer Modern Family of Fonts): Donald E. Knuth
20
+ Author: Bogus\l{}aw Jackowski and Janusz M. Nowacki
21
+ Version: 2.005
22
+ Date: 21 III 2021
23
+ Downloads: http://www.gust.org.pl/projects/e-foundry/latin-modern/
24
+ License:
25
+ % Copyright 2003--2021 by B. Jackowski and J.M. Nowacki
26
+ % (on behalf of TeX Users Groups).
27
+ % This work is released under the GUST Font License
28
+ % -- see GUST-FONT-LICENSE.txt.
29
+ % This work has the LPPL maintenance status "maintained".
30
+ % The Current Maintainer of this work is Bogus\l{}aw Jackowski
31
+ % This work consists of the files listed in the MANIFEST.txt file.
32
+
33
+ [...]
34
+
35
+ The current LM package contains the most recent version
36
+ of the Latin Modern family of fonts in the PostScript Type 1 and
37
+ OpenType format. The fonts are based on Donald E. Knuth's Computer Modern
38
+ fonts in the PostScript Type 1 format, released into public domain by the
39
+ American Mathematical Society (for the history of the outline version of
40
+ the CM fonts see, e.g., http://www.math.utah.edu/~beebe/fonts/bluesky.html ).
41
+ The project is supported by TeX users groups: CSTUG, DANTE eV, GUST,
42
+ GUTenberg, NTG, and TUG.
43
+
44
+
45
+ The current README-Latin-Modern-Math.txt says
46
+ License:
47
+ % Copyright 2012--2014 for the Latin Modern math extensions by B. Jackowski,
48
+ % P. Strzelczyk and P. Pianowski (on behalf of TeX Users Groups).
49
+ %
50
+ % This work can be freely used and distributed under
51
+ % the GUST Font License (GFL -- see GUST-FONT-LICENSE.txt)
52
+ % which is actually an instance of the LaTeX Project Public License
53
+ % (LPPL -- see http://www.latex-project.org/lppl.txt).
54
+ %
55
+ % This work has the maintenance status "maintained". The Current Maintainer
56
+ % of this work is Bogus\l{}aw Jackowski, Piotr Strzelczyk and Piotr Pianowski.
57
+ %
58
+ % This work consists of the files listed
59
+ % in the MANIFEST-Latin-Modern-Math.txt file.
60
+
61
+
62
+ See the appendix B for the GUST Font License.
63
+
64
+ Please read the appendix A below if you want to examine the licensing terms
65
+ for the Computer Modern fonts in Type 1 format on which the Latin Modern fonts
66
+ are based.
67
+
68
+
69
+ Debian packaging
70
+ ----------------
71
+
72
+ Copyright (c) 2004-2007 Florent Rougon
73
+ Copyright (c) 2005-2019 Norbert Preining
74
+ Copyright (c) 2021-2022 Hilmar Preusse
75
+
76
+ This program is free software; you can redistribute it and/or modify
77
+ it under the terms of the GNU General Public License as published by
78
+ the Free Software Foundation; version 2 dated June, 1991.
79
+
80
+ This program is distributed in the hope that it will be useful, but
81
+ WITHOUT ANY WARRANTY; without even the implied warranty of
82
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
83
+ General Public License for more details.
84
+
85
+ You should have received a copy of the GNU General Public License
86
+ along with this program; see the file COPYING. If not, write to the
87
+ Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
88
+ Boston, MA 02110-1301 USA.
89
+
90
+ On Debian systems, the complete text of the GNU General Public License version
91
+ 2 can be found in `/usr/share/common-licenses/GPL-2'.
92
+
93
+
94
+ Appendix A -- Licensing terms for the Computer Modern fonts in Type 1 format
95
+ on which the Latin Modern fonts are based.
96
+ -----------------------------------------------------------------------------
97
+
98
+ Running t1disasm (from the t1utils package) on
99
+ /usr/share/texmf/fonts/type1/bluesky/cm/*.pfb and
100
+ /usr/share/texmf/fonts/type1/bluesky/cmextra/*.pfb yields:
101
+
102
+ Copyright (C) 1997 American Mathematical Society. All Rights Reserved
103
+
104
+ [ Florent Rougon's note: well, for
105
+ /usr/share/texmf/fonts/type1/bluesky/cm/cmb{,sy}10.pfb, you will only get
106
+ one space between "Society." and "All" (with tetex-extra 2.0.2-5.1). ;-) ]
107
+
108
+ The precise distribution conditions for these fonts can be found in
109
+ /usr/share/doc/texmf/fonts/bluesky/README from the tetex-doc package. I will
110
+ duplicate here the relevant excerpt for your convenience:
111
+
112
+ The PostScript Type 1 implementation of the Computer Modern fonts produced
113
+ by and previously distributed by Blue Sky Research and Y&Y, Inc. are now
114
+ freely available for general use. This has been accomplished through the
115
+ cooperation of a consortium of scientific publishers with Blue Sky Research
116
+ and Y&Y. Members of this consortium include:
117
+
118
+ Elsevier Science
119
+ IBM Corporation
120
+ Society for Industrial and Applied Mathematics (SIAM)
121
+ Springer-Verlag
122
+ American Mathematical Society (AMS)
123
+
124
+ In order to assure the authenticity of these fonts, copyright will be held
125
+ by the American Mathematical Society. This is not meant to restrict in any
126
+ way the legitimate use of the fonts, such as (but not limited to) electronic
127
+ distribution of documents containing these fonts, inclusion of these fonts
128
+ into other public domain or commercial font collections or computer
129
+ applications, use of the outline data to create derivative fonts and/or
130
+ faces, etc. However, the AMS does require that the AMS copyright notice be
131
+ removed from any derivative versions of the fonts which have been altered in
132
+ any way. In addition, to ensure the fidelity of TeX documents using Computer
133
+ Modern fonts, Professor Donald Knuth, creator of the Computer Modern faces,
134
+ has requested that any alterations which yield different font metrics be
135
+ given a different name.
136
+
137
+
138
+ Appendix B -- GUST Font License
139
+ -------------------------------
140
+
141
+ What follows is the exact contents of GUST-FONT-LICENSE.txt from the
142
+ upstream distribution of the Latin Modern fonts.
143
+
144
+ % This is version 1.0, dated 22 June 2009, of the GUST Font License.
145
+ % (GUST is the Polish TeX Users Group, http://www.gust.org.pl)
146
+ %
147
+ % For the most recent version of this license see
148
+ % http://www.gust.org.pl/fonts/licenses/GUST-FONT-LICENSE.txt
149
+ % or
150
+ % http://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt
151
+ %
152
+ % This work may be distributed and/or modified under the conditions
153
+ % of the LaTeX Project Public License, either version 1.3c of this
154
+ % license or (at your option) any later version.
155
+ %
156
+ % Please also observe the following clause:
157
+ % 1) it is requested, but not legally required, that derived works be
158
+ % distributed only after changing the names of the fonts comprising this
159
+ % work and given in an accompanying "manifest", and that the
160
+ % files comprising the Work, as listed in the manifest, also be given
161
+ % new names. Any exceptions to this request are also given in the
162
+ % manifest.
163
+ %
164
+ % We recommend the manifest be given in a separate file named
165
+ % MANIFEST-<fontid>.txt, where <fontid> is some unique identification
166
+ % of the font family. If a separate "readme" file accompanies the Work,
167
+ % we recommend a name of the form README-<fontid>.txt.
168
+ %
169
+ % The latest version of the LaTeX Project Public License is in
170
+ % http://www.latex-project.org/lppl.txt and version 1.3c or later
171
+ % is part of all distributions of LaTeX version 2006/05/20 or later.
@@ -0,0 +1,180 @@
1
+ """The casebook: approved corrections as an append-only JSON-lines store. See `specs/casebook.md`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import UTC, date, datetime, timedelta
7
+ from pathlib import Path
8
+
9
+ from . import config as config_mod
10
+ from .checks import Finding
11
+ from .checks.ids import finding
12
+
13
+ STATUSES = ("active", "distilled", "contradicted", "pruned")
14
+ REQUIRED = ("artifact", "part", "topics", "hook", "problem", "before", "after", "lesson")
15
+ CASES_FILE = "cases.jsonl"
16
+ USE_FILE = "cases.use.jsonl"
17
+
18
+
19
+ class CaseError(Exception):
20
+ """A case command cannot proceed."""
21
+
22
+
23
+ def _path(name: str) -> Path:
24
+ return config_mod.guides_private() / name
25
+
26
+
27
+ def _lines(name: str) -> list[dict]:
28
+ path = _path(name)
29
+ if not path.is_file():
30
+ return []
31
+ return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
32
+
33
+
34
+ def _append(name: str, entry: dict) -> None:
35
+ path = _path(name)
36
+ path.parent.mkdir(parents=True, exist_ok=True)
37
+ with path.open("a", encoding="utf-8") as fh:
38
+ fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
39
+
40
+
41
+ def load() -> dict[str, dict]:
42
+ """The current state of every case: its lines folded in order. Insertion order is first-seen."""
43
+ cases: dict[str, dict] = {}
44
+ for entry in _lines(CASES_FILE):
45
+ cases.setdefault(entry["id"], {}).update(entry)
46
+ return cases
47
+
48
+
49
+ def add(root: Path, payload: dict, known_topics: set[str], *, today: date | None = None) -> str:
50
+ missing = [k for k in REQUIRED if not payload.get(k)]
51
+ if missing:
52
+ raise CaseError(f"missing field(s): {', '.join(missing)}")
53
+ if not isinstance(payload["topics"], list):
54
+ raise CaseError("topics must be a list")
55
+ unknown = [t for t in payload["topics"] if t not in known_topics]
56
+ if unknown:
57
+ raise CaseError(f"unknown topic(s): {', '.join(unknown)} (have: {', '.join(sorted(known_topics))})")
58
+ for key in ("hook", "lesson"):
59
+ if "\n" in payload[key].strip():
60
+ raise CaseError(f"the {key} must be one line")
61
+ card = config_mod.load_card(root, payload["artifact"])
62
+ cases = load()
63
+ number = max((int(i.removeprefix("c-")) for i in cases), default=0) + 1
64
+ entry = {
65
+ "id": f"c-{number:04d}",
66
+ "date": (today or datetime.now(UTC).date()).isoformat(),
67
+ **{k: payload[k] for k in REQUIRED},
68
+ "modality": card.modality,
69
+ "audience": card.audience,
70
+ "contradicts": payload.get("contradicts", []),
71
+ "status": "active",
72
+ }
73
+ _append(CASES_FILE, entry)
74
+ return entry["id"]
75
+
76
+
77
+ EDITABLE = ("hook", "lesson", "problem", "topics")
78
+
79
+
80
+ def edit(case_id: str, fields: dict, known_topics: set[str]) -> None:
81
+ """Appends an update to a case. Only the hook, lesson, problem and topics can change; the before
82
+ and after stay as the author left them."""
83
+ if case_id not in load():
84
+ raise CaseError(f"unknown case '{case_id}'")
85
+ bad = set(fields) - set(EDITABLE)
86
+ if bad or not fields:
87
+ raise CaseError(f"edit takes {', '.join(EDITABLE)}; got {', '.join(sorted(bad)) or 'nothing'}")
88
+ for key in ("hook", "lesson"):
89
+ if key in fields and ("\n" in str(fields[key]).strip() or not str(fields[key]).strip()):
90
+ raise CaseError(f"the {key} must be one non-empty line")
91
+ unknown = [t for t in fields.get("topics", []) if t not in known_topics]
92
+ if unknown:
93
+ raise CaseError(f"unknown topic(s): {', '.join(unknown)} (have: {', '.join(sorted(known_topics))})")
94
+ _append(CASES_FILE, {"id": case_id, **fields})
95
+
96
+
97
+ def mark(case_id: str, status: str) -> None:
98
+ if status not in STATUSES:
99
+ raise CaseError(f"unknown status '{status}' (have: {', '.join(STATUSES)})")
100
+ if case_id not in load():
101
+ raise CaseError(f"unknown case '{case_id}'")
102
+ _append(CASES_FILE, {"id": case_id, "status": status})
103
+
104
+
105
+ def list_lines(topic: str | None = None, status: str | None = None) -> list[str]:
106
+ return [
107
+ f"{c['id']} {c['status']} {','.join(c['topics'])} {c['audience']} {c.get('hook', '-')} | {c['lesson']}"
108
+ for c in load().values()
109
+ if (topic is None or topic in c["topics"]) and (status is None or c["status"] == status)
110
+ ]
111
+
112
+
113
+ def retrieve(
114
+ *, topics: set[str], modality: str, audience: str | None, artifact: str, max_cases: int
115
+ ) -> list[dict]:
116
+ """Active or distilled cases on the loaded topics whose audience and modality match or are
117
+ absent, best first: same artifact, same audience, same modality, most recent."""
118
+ hits = [
119
+ c for c in load().values()
120
+ if c["status"] in ("active", "distilled")
121
+ and topics & set(c["topics"])
122
+ and (not c.get("audience") or c["audience"] == audience)
123
+ and (not c.get("modality") or c["modality"] == modality)
124
+ ]
125
+ hits.sort(
126
+ key=lambda c: (c["artifact"] == artifact, c.get("audience") == audience, c.get("modality") == modality, c["date"], c["id"]),
127
+ reverse=True,
128
+ )
129
+ return hits[:max_cases]
130
+
131
+
132
+ def show(case_ids: list[str], artifact: str) -> str:
133
+ """The cases in full as XML, in the order asked; logs one use per case."""
134
+ cases = load()
135
+ unknown = [i for i in case_ids if i not in cases]
136
+ if unknown:
137
+ raise CaseError(f"unknown case(s): {', '.join(unknown)}")
138
+ log_use(case_ids, artifact)
139
+ return "\n\n".join(
140
+ f'<case id="{i}">\n'
141
+ + "\n".join(f"<{k}>{cases[i][k]}</{k}>" for k in ("problem", "after", "lesson"))
142
+ + "\n</case>"
143
+ for i in case_ids
144
+ )
145
+
146
+
147
+ def log_use(case_ids: list[str], artifact: str, *, today: date | None = None) -> None:
148
+ stamp = (today or datetime.now(UTC).date()).isoformat()
149
+ for case_id in case_ids:
150
+ _append(USE_FILE, {"case": case_id, "date": stamp, "artifact": artifact})
151
+
152
+
153
+ def lint(*, distill_min: int, stale_days: int | None, today: date | None = None) -> list[Finding]:
154
+ today = today or datetime.now(UTC).date()
155
+ cases = load()
156
+ first_line: dict[str, int] = {}
157
+ for number, entry in enumerate(_lines(CASES_FILE), 1):
158
+ first_line.setdefault(entry["id"], number)
159
+ active = [c for c in cases.values() if c["status"] == "active"]
160
+ findings: list[Finding] = []
161
+ groups: dict[tuple[str, str], list[str]] = {}
162
+ for c in active:
163
+ for topic in c["topics"]:
164
+ groups.setdefault((topic, c.get("audience", "")), []).append(c["id"])
165
+ for (topic, audience), ids in sorted(groups.items()):
166
+ if len(ids) >= distill_min:
167
+ findings.append(finding(
168
+ "case-distill", CASES_FILE, first_line[ids[0]],
169
+ f"{len(ids)} active cases on '{topic}' for '{audience}' ({', '.join(ids)}): propose a principle",
170
+ ))
171
+ if stale_days is not None:
172
+ last_use: dict[str, str] = {}
173
+ for use in _lines(USE_FILE):
174
+ last_use[use["case"]] = max(last_use.get(use["case"], ""), use["date"])
175
+ cutoff = today - timedelta(days=stale_days)
176
+ for c in active:
177
+ seen = date.fromisoformat(last_use.get(c["id"], c["date"]))
178
+ if seen < cutoff:
179
+ findings.append(finding("case-stale", CASES_FILE, first_line[c["id"]], f"{c['id']} was not retrieved since {seen}"))
180
+ return findings
@@ -0,0 +1,3 @@
1
+ from .finding import CheckFailed, Finding, Severity
2
+
3
+ __all__ = ["CheckFailed", "Finding", "Severity"]
@@ -0,0 +1,28 @@
1
+ """One reported problem. See `specs/checks.md#finding`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Literal
7
+
8
+ Severity = Literal["error", "warning", "probe", "resolved"]
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class Finding:
13
+ file: str # relative to parla/
14
+ line: int
15
+ severity: Severity
16
+ id: str
17
+ message: str
18
+
19
+ def format(self) -> str:
20
+ return f"{self.file}:{self.line}: {self.severity} {self.id}: {self.message}"
21
+
22
+
23
+ class CheckFailed(Exception):
24
+ """A check reported an error finding; nothing was built."""
25
+
26
+ def __init__(self, findings: list[Finding]):
27
+ self.findings = findings
28
+ super().__init__("\n".join(f.format() for f in findings if f.severity == "error"))