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.
- parla-0.0.1/LICENSE +13 -0
- parla-0.0.1/MANIFEST.in +2 -0
- parla-0.0.1/PKG-INFO +94 -0
- parla-0.0.1/README.md +71 -0
- parla-0.0.1/pyproject.toml +54 -0
- parla-0.0.1/setup.cfg +4 -0
- parla-0.0.1/src/parla/__init__.py +3 -0
- parla-0.0.1/src/parla/assets/fonts/LICENSE-Latin-Modern.txt +171 -0
- parla-0.0.1/src/parla/assets/fonts/lmmono10-italic.otf +0 -0
- parla-0.0.1/src/parla/assets/fonts/lmmono10-regular.otf +0 -0
- parla-0.0.1/src/parla/assets/fonts/lmmonolt10-bold.otf +0 -0
- parla-0.0.1/src/parla/assets/fonts/lmsans10-bold.otf +0 -0
- parla-0.0.1/src/parla/assets/fonts/lmsans10-boldoblique.otf +0 -0
- parla-0.0.1/src/parla/assets/fonts/lmsans10-oblique.otf +0 -0
- parla-0.0.1/src/parla/assets/fonts/lmsans10-regular.otf +0 -0
- parla-0.0.1/src/parla/casebook.py +180 -0
- parla-0.0.1/src/parla/checks/__init__.py +3 -0
- parla-0.0.1/src/parla/checks/finding.py +28 -0
- parla-0.0.1/src/parla/checks/ids.py +59 -0
- parla-0.0.1/src/parla/checks/shared.py +57 -0
- parla-0.0.1/src/parla/cli.py +559 -0
- parla-0.0.1/src/parla/config.py +371 -0
- parla-0.0.1/src/parla/constraints.py +432 -0
- parla-0.0.1/src/parla/data/config.example.toml +32 -0
- parla-0.0.1/src/parla/docker/Dockerfile +49 -0
- parla-0.0.1/src/parla/docker/tl-packages.txt +11 -0
- parla-0.0.1/src/parla/docker/tl.profile +14 -0
- parla-0.0.1/src/parla/engines/__init__.py +0 -0
- parla-0.0.1/src/parla/engines/base.py +97 -0
- parla-0.0.1/src/parla/engines/latex/__init__.py +0 -0
- parla-0.0.1/src/parla/engines/latex/autoload.py +62 -0
- parla-0.0.1/src/parla/engines/latex/bib.py +348 -0
- parla-0.0.1/src/parla/engines/latex/build.py +269 -0
- parla-0.0.1/src/parla/engines/latex/checks.py +446 -0
- parla-0.0.1/src/parla/engines/latex/diff.py +100 -0
- parla-0.0.1/src/parla/engines/latex/engine.py +82 -0
- parla-0.0.1/src/parla/engines/latex/export.py +298 -0
- parla-0.0.1/src/parla/engines/latex/image.py +90 -0
- parla-0.0.1/src/parla/engines/latex/links.py +36 -0
- parla-0.0.1/src/parla/engines/none/__init__.py +0 -0
- parla-0.0.1/src/parla/engines/none/engine.py +36 -0
- parla-0.0.1/src/parla/engines/registry.py +22 -0
- parla-0.0.1/src/parla/engines/svg/__init__.py +0 -0
- parla-0.0.1/src/parla/engines/svg/build.py +102 -0
- parla-0.0.1/src/parla/engines/svg/checks.py +460 -0
- parla-0.0.1/src/parla/engines/svg/engine.py +56 -0
- parla-0.0.1/src/parla/engines/svg/fonts.py +58 -0
- parla-0.0.1/src/parla/frontmatter.py +16 -0
- parla-0.0.1/src/parla/guides/__init__.py +3 -0
- parla-0.0.1/src/parla/guides/audience/default.md +7 -0
- parla-0.0.1/src/parla/guides/constraints.jsonl +6 -0
- parla-0.0.1/src/parla/guides/edit.py +191 -0
- parla-0.0.1/src/parla/guides/engine/beamer.md +8 -0
- parla-0.0.1/src/parla/guides/engine/cv.md +12 -0
- parla-0.0.1/src/parla/guides/engine/latex.md +11 -0
- parla-0.0.1/src/parla/guides/engine/none.md +7 -0
- parla-0.0.1/src/parla/guides/engine/svg.md +10 -0
- parla-0.0.1/src/parla/guides/general.md +4 -0
- parla-0.0.1/src/parla/guides/lint.py +115 -0
- parla-0.0.1/src/parla/guides/modality/cv.md +24 -0
- parla-0.0.1/src/parla/guides/modality/diagram.md +18 -0
- parla-0.0.1/src/parla/guides/modality/paper.md +23 -0
- parla-0.0.1/src/parla/guides/modality/short-paper.md +9 -0
- parla-0.0.1/src/parla/guides/modality/social-post.md +18 -0
- parla-0.0.1/src/parla/guides/modality/talk.md +24 -0
- parla-0.0.1/src/parla/guides/modality/thesis.md +11 -0
- parla-0.0.1/src/parla/guides/move/compress.md +15 -0
- parla-0.0.1/src/parla/guides/move/elaborate.md +7 -0
- parla-0.0.1/src/parla/guides/move/hedge.md +9 -0
- parla-0.0.1/src/parla/guides/move/sharpen.md +9 -0
- parla-0.0.1/src/parla/guides/pages.py +151 -0
- parla-0.0.1/src/parla/guides/procedure/condense-table.md +7 -0
- parla-0.0.1/src/parla/guides/procedure/edit.md +7 -0
- parla-0.0.1/src/parla/guides/procedure/fit-pages.md +7 -0
- parla-0.0.1/src/parla/guides/procedure/ground.md +7 -0
- parla-0.0.1/src/parla/guides/procedure/read-figure.md +7 -0
- parla-0.0.1/src/parla/guides/procedure/read-paper.md +7 -0
- parla-0.0.1/src/parla/guides/rules.py +129 -0
- parla-0.0.1/src/parla/guides/show.py +391 -0
- parla-0.0.1/src/parla/guides/topic/abstract.md +10 -0
- parla-0.0.1/src/parla/guides/topic/bullets.md +6 -0
- parla-0.0.1/src/parla/guides/topic/captions.md +6 -0
- parla-0.0.1/src/parla/guides/topic/citations.md +6 -0
- parla-0.0.1/src/parla/guides/topic/claims.md +6 -0
- parla-0.0.1/src/parla/guides/topic/contributions.md +6 -0
- parla-0.0.1/src/parla/guides/topic/deck.md +8 -0
- parla-0.0.1/src/parla/guides/topic/diagram.md +8 -0
- parla-0.0.1/src/parla/guides/topic/figures.md +6 -0
- parla-0.0.1/src/parla/guides/topic/flow.md +6 -0
- parla-0.0.1/src/parla/guides/topic/selection.md +6 -0
- parla-0.0.1/src/parla/guides/topic/skills.md +8 -0
- parla-0.0.1/src/parla/guides/topic/summary.md +6 -0
- parla-0.0.1/src/parla/guides/topic/tables.md +6 -0
- parla-0.0.1/src/parla/guides/topic/terminology.md +6 -0
- parla-0.0.1/src/parla/guides/trait/narrative/engineering.md +5 -0
- parla-0.0.1/src/parla/guides/trait/narrative/executive.md +5 -0
- parla-0.0.1/src/parla/guides/trait/narrative/scientific.md +5 -0
- parla-0.0.1/src/parla/guides/trait/narrative/teaching.md +5 -0
- parla-0.0.1/src/parla/guides/trait/pacing/balanced.md +5 -0
- parla-0.0.1/src/parla/guides/trait/pacing/generous.md +5 -0
- parla-0.0.1/src/parla/guides/trait/pacing/tight.md +5 -0
- parla-0.0.1/src/parla/guides/trait/reader/cross-domain.md +5 -0
- parla-0.0.1/src/parla/guides/trait/reader/newcomer.md +5 -0
- parla-0.0.1/src/parla/guides/trait/reader/peer.md +5 -0
- parla-0.0.1/src/parla/guides/trait/reader/screener.md +5 -0
- parla-0.0.1/src/parla/guides/trait/visual/academic.md +5 -0
- parla-0.0.1/src/parla/guides/trait/visual/keynote.md +5 -0
- parla-0.0.1/src/parla/jobs.py +170 -0
- parla-0.0.1/src/parla/names.py +25 -0
- parla-0.0.1/src/parla/py.typed +0 -0
- parla-0.0.1/src/parla/router.py +237 -0
- parla-0.0.1/src/parla/settings.py +93 -0
- parla-0.0.1/src/parla/skills/parla/SKILL.md +108 -0
- parla-0.0.1/src/parla/skills/parla/artifacts.md +25 -0
- parla-0.0.1/src/parla/skills/parla/asking.md +27 -0
- parla-0.0.1/src/parla/skills/parla/constraints.md +25 -0
- parla-0.0.1/src/parla/skills/parla/distilling.md +40 -0
- parla-0.0.1/src/parla/skills/parla/jobs.md +37 -0
- parla-0.0.1/src/parla/skills/parla/learning.md +59 -0
- parla-0.0.1/src/parla/skills/parla/moves.md +17 -0
- parla-0.0.1/src/parla/skills/parla/relocating.md +29 -0
- parla-0.0.1/src/parla/skills/parla/seeding.md +9 -0
- parla-0.0.1/src/parla/skills/parla/suite.md +38 -0
- parla-0.0.1/src/parla/skills/parla/worker.md +30 -0
- parla-0.0.1/src/parla/template.py +275 -0
- parla-0.0.1/src/parla/texmf/bibtex/bib/arxiv-references.bib +27 -0
- parla-0.0.1/src/parla/texmf/tex/latex/arxiv-style/License.txt +21 -0
- parla-0.0.1/src/parla/texmf/tex/latex/arxiv-style/README.md +49 -0
- parla-0.0.1/src/parla/texmf/tex/latex/arxiv-style/arxiv.sty +262 -0
- parla-0.0.1/src/parla/texmf/tex/latex/arxiv-style/orcid.pdf +0 -0
- parla-0.0.1/src/parla/texmf/tex/latex/arxiv-style/template.pdf +0 -0
- parla-0.0.1/src/parla/texmf/tex/latex/arxiv-style/template.tex +214 -0
- parla-0.0.1/src/parla/tools/__init__.py +0 -0
- parla-0.0.1/src/parla/tools/claims.py +124 -0
- parla-0.0.1/src/parla/tools/formula.py +84 -0
- parla-0.0.1/src/parla/tools/measure.py +72 -0
- parla-0.0.1/src/parla/tools/pdf/__init__.py +39 -0
- parla-0.0.1/src/parla/tools/pdf/base.py +28 -0
- parla-0.0.1/src/parla/tools/pdf/heuristics.py +529 -0
- parla-0.0.1/src/parla/tools/pdf/pdftotext.py +131 -0
- parla-0.0.1/src/parla/tools/pdf/pymupdf.py +127 -0
- parla-0.0.1/src/parla/tools/pdf/pymupdf4llm.py +112 -0
- parla-0.0.1/src/parla/tools/sections.py +909 -0
- parla-0.0.1/src/parla/tools/source.py +109 -0
- parla-0.0.1/src/parla/tools/table.py +64 -0
- parla-0.0.1/src/parla/tools/table_fit.py +181 -0
- parla-0.0.1/src/parla/tools/table_parse.py +357 -0
- parla-0.0.1/src/parla/tools/table_render.py +191 -0
- parla-0.0.1/src/parla.egg-info/PKG-INFO +94 -0
- parla-0.0.1/src/parla.egg-info/SOURCES.txt +214 -0
- parla-0.0.1/src/parla.egg-info/dependency_links.txt +1 -0
- parla-0.0.1/src/parla.egg-info/entry_points.txt +2 -0
- parla-0.0.1/src/parla.egg-info/requires.txt +14 -0
- parla-0.0.1/src/parla.egg-info/top_level.txt +1 -0
- parla-0.0.1/tests/conftest.py +42 -0
- parla-0.0.1/tests/fixtures/guides/private/audience/ai-rd.md +8 -0
- parla-0.0.1/tests/fixtures/guides/private/audience/icra.md +8 -0
- parla-0.0.1/tests/fixtures/guides/private/audience/linkedin.md +8 -0
- parla-0.0.1/tests/fixtures/guides/private/engine/beamer.md +8 -0
- parla-0.0.1/tests/fixtures/guides/private/engine/latex.md +8 -0
- parla-0.0.1/tests/fixtures/guides/private/engine/none.md +8 -0
- parla-0.0.1/tests/fixtures/guides/private/engine/svg.md +8 -0
- parla-0.0.1/tests/fixtures/guides/private/general.md +7 -0
- parla-0.0.1/tests/fixtures/guides/private/modality/cv.md +8 -0
- parla-0.0.1/tests/fixtures/guides/private/modality/paper.md +8 -0
- parla-0.0.1/tests/fixtures/guides/private/modality/social-post.md +8 -0
- parla-0.0.1/tests/fixtures/guides/private/topic/contributions.md +9 -0
- parla-0.0.1/tests/fixtures/guides/private/topic/evidence.md +9 -0
- parla-0.0.1/tests/fixtures/guides/private/topic/positioning.md +25 -0
- parla-0.0.1/tests/fixtures/guides/private/trait/reader/cross-domain.md +8 -0
- parla-0.0.1/tests/fixtures/guides/private/trait/reader/peer.md +8 -0
- parla-0.0.1/tests/fixtures/guides/public/audience/ai-rd.md +13 -0
- parla-0.0.1/tests/fixtures/guides/public/audience/default.md +7 -0
- parla-0.0.1/tests/fixtures/guides/public/audience/icra.md +16 -0
- parla-0.0.1/tests/fixtures/guides/public/audience/linkedin.md +8 -0
- parla-0.0.1/tests/fixtures/guides/public/constraints.jsonl +1 -0
- parla-0.0.1/tests/fixtures/guides/public/engine/beamer.md +7 -0
- parla-0.0.1/tests/fixtures/guides/public/engine/latex.md +7 -0
- parla-0.0.1/tests/fixtures/guides/public/engine/none.md +7 -0
- parla-0.0.1/tests/fixtures/guides/public/engine/svg.md +7 -0
- parla-0.0.1/tests/fixtures/guides/public/general.md +4 -0
- parla-0.0.1/tests/fixtures/guides/public/modality/cv.md +14 -0
- parla-0.0.1/tests/fixtures/guides/public/modality/diagram.md +14 -0
- parla-0.0.1/tests/fixtures/guides/public/modality/paper.md +17 -0
- parla-0.0.1/tests/fixtures/guides/public/modality/social-post.md +14 -0
- parla-0.0.1/tests/fixtures/guides/public/modality/talk.md +17 -0
- parla-0.0.1/tests/fixtures/guides/public/topic/positioning.md +6 -0
- parla-0.0.1/tests/fixtures/guides/public/topic/slides-only.md +8 -0
- parla-0.0.1/tests/fixtures/guides/public/trait/reader/cross-domain.md +5 -0
- parla-0.0.1/tests/fixtures/guides/public/trait/reader/peer.md +5 -0
- parla-0.0.1/tests/fixtures/svg/knn-graph.svg +92 -0
- parla-0.0.1/tests/test_autoload.py +61 -0
- parla-0.0.1/tests/test_bib.py +170 -0
- parla-0.0.1/tests/test_build.py +219 -0
- parla-0.0.1/tests/test_checks_latex.py +249 -0
- parla-0.0.1/tests/test_cli.py +240 -0
- parla-0.0.1/tests/test_constraints.py +288 -0
- parla-0.0.1/tests/test_conventions.py +65 -0
- parla-0.0.1/tests/test_diff.py +114 -0
- parla-0.0.1/tests/test_docker.py +68 -0
- parla-0.0.1/tests/test_engines.py +286 -0
- parla-0.0.1/tests/test_export.py +299 -0
- parla-0.0.1/tests/test_guides.py +687 -0
- parla-0.0.1/tests/test_image.py +41 -0
- parla-0.0.1/tests/test_jobs.py +169 -0
- parla-0.0.1/tests/test_links.py +103 -0
- parla-0.0.1/tests/test_packaging.py +42 -0
- parla-0.0.1/tests/test_settings.py +73 -0
- parla-0.0.1/tests/test_skill.py +32 -0
- parla-0.0.1/tests/test_smoke.py +2 -0
- parla-0.0.1/tests/test_svg_engine.py +211 -0
- parla-0.0.1/tests/test_table_fit_and_render.py +295 -0
- parla-0.0.1/tests/test_table_parse.py +151 -0
- parla-0.0.1/tests/test_template_init.py +189 -0
- parla-0.0.1/tests/test_tools.py +260 -0
- 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.
|
parla-0.0.1/MANIFEST.in
ADDED
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,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.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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,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"))
|