parla 0.0.1__py3-none-any.whl
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/__init__.py +3 -0
- parla/assets/fonts/LICENSE-Latin-Modern.txt +171 -0
- parla/assets/fonts/lmmono10-italic.otf +0 -0
- parla/assets/fonts/lmmono10-regular.otf +0 -0
- parla/assets/fonts/lmmonolt10-bold.otf +0 -0
- parla/assets/fonts/lmsans10-bold.otf +0 -0
- parla/assets/fonts/lmsans10-boldoblique.otf +0 -0
- parla/assets/fonts/lmsans10-oblique.otf +0 -0
- parla/assets/fonts/lmsans10-regular.otf +0 -0
- parla/casebook.py +180 -0
- parla/checks/__init__.py +3 -0
- parla/checks/finding.py +28 -0
- parla/checks/ids.py +59 -0
- parla/checks/shared.py +57 -0
- parla/cli.py +559 -0
- parla/config.py +371 -0
- parla/constraints.py +432 -0
- parla/data/config.example.toml +32 -0
- parla/docker/Dockerfile +49 -0
- parla/docker/tl-packages.txt +11 -0
- parla/docker/tl.profile +14 -0
- parla/engines/__init__.py +0 -0
- parla/engines/base.py +97 -0
- parla/engines/latex/__init__.py +0 -0
- parla/engines/latex/autoload.py +62 -0
- parla/engines/latex/bib.py +348 -0
- parla/engines/latex/build.py +269 -0
- parla/engines/latex/checks.py +446 -0
- parla/engines/latex/diff.py +100 -0
- parla/engines/latex/engine.py +82 -0
- parla/engines/latex/export.py +298 -0
- parla/engines/latex/image.py +90 -0
- parla/engines/latex/links.py +36 -0
- parla/engines/none/__init__.py +0 -0
- parla/engines/none/engine.py +36 -0
- parla/engines/registry.py +22 -0
- parla/engines/svg/__init__.py +0 -0
- parla/engines/svg/build.py +102 -0
- parla/engines/svg/checks.py +460 -0
- parla/engines/svg/engine.py +56 -0
- parla/engines/svg/fonts.py +58 -0
- parla/frontmatter.py +16 -0
- parla/guides/__init__.py +3 -0
- parla/guides/audience/default.md +7 -0
- parla/guides/constraints.jsonl +6 -0
- parla/guides/edit.py +191 -0
- parla/guides/engine/beamer.md +8 -0
- parla/guides/engine/cv.md +12 -0
- parla/guides/engine/latex.md +11 -0
- parla/guides/engine/none.md +7 -0
- parla/guides/engine/svg.md +10 -0
- parla/guides/general.md +4 -0
- parla/guides/lint.py +115 -0
- parla/guides/modality/cv.md +24 -0
- parla/guides/modality/diagram.md +18 -0
- parla/guides/modality/paper.md +23 -0
- parla/guides/modality/short-paper.md +9 -0
- parla/guides/modality/social-post.md +18 -0
- parla/guides/modality/talk.md +24 -0
- parla/guides/modality/thesis.md +11 -0
- parla/guides/move/compress.md +15 -0
- parla/guides/move/elaborate.md +7 -0
- parla/guides/move/hedge.md +9 -0
- parla/guides/move/sharpen.md +9 -0
- parla/guides/pages.py +151 -0
- parla/guides/procedure/condense-table.md +7 -0
- parla/guides/procedure/edit.md +7 -0
- parla/guides/procedure/fit-pages.md +7 -0
- parla/guides/procedure/ground.md +7 -0
- parla/guides/procedure/read-figure.md +7 -0
- parla/guides/procedure/read-paper.md +7 -0
- parla/guides/rules.py +129 -0
- parla/guides/show.py +391 -0
- parla/guides/topic/abstract.md +10 -0
- parla/guides/topic/bullets.md +6 -0
- parla/guides/topic/captions.md +6 -0
- parla/guides/topic/citations.md +6 -0
- parla/guides/topic/claims.md +6 -0
- parla/guides/topic/contributions.md +6 -0
- parla/guides/topic/deck.md +8 -0
- parla/guides/topic/diagram.md +8 -0
- parla/guides/topic/figures.md +6 -0
- parla/guides/topic/flow.md +6 -0
- parla/guides/topic/selection.md +6 -0
- parla/guides/topic/skills.md +8 -0
- parla/guides/topic/summary.md +6 -0
- parla/guides/topic/tables.md +6 -0
- parla/guides/topic/terminology.md +6 -0
- parla/guides/trait/narrative/engineering.md +5 -0
- parla/guides/trait/narrative/executive.md +5 -0
- parla/guides/trait/narrative/scientific.md +5 -0
- parla/guides/trait/narrative/teaching.md +5 -0
- parla/guides/trait/pacing/balanced.md +5 -0
- parla/guides/trait/pacing/generous.md +5 -0
- parla/guides/trait/pacing/tight.md +5 -0
- parla/guides/trait/reader/cross-domain.md +5 -0
- parla/guides/trait/reader/newcomer.md +5 -0
- parla/guides/trait/reader/peer.md +5 -0
- parla/guides/trait/reader/screener.md +5 -0
- parla/guides/trait/visual/academic.md +5 -0
- parla/guides/trait/visual/keynote.md +5 -0
- parla/jobs.py +170 -0
- parla/names.py +25 -0
- parla/py.typed +0 -0
- parla/router.py +237 -0
- parla/settings.py +93 -0
- parla/skills/parla/SKILL.md +108 -0
- parla/skills/parla/artifacts.md +25 -0
- parla/skills/parla/asking.md +27 -0
- parla/skills/parla/constraints.md +25 -0
- parla/skills/parla/distilling.md +40 -0
- parla/skills/parla/jobs.md +37 -0
- parla/skills/parla/learning.md +59 -0
- parla/skills/parla/moves.md +17 -0
- parla/skills/parla/relocating.md +29 -0
- parla/skills/parla/seeding.md +9 -0
- parla/skills/parla/suite.md +38 -0
- parla/skills/parla/worker.md +30 -0
- parla/template.py +275 -0
- parla/texmf/bibtex/bib/arxiv-references.bib +27 -0
- parla/texmf/tex/latex/arxiv-style/License.txt +21 -0
- parla/texmf/tex/latex/arxiv-style/README.md +49 -0
- parla/texmf/tex/latex/arxiv-style/arxiv.sty +262 -0
- parla/texmf/tex/latex/arxiv-style/orcid.pdf +0 -0
- parla/texmf/tex/latex/arxiv-style/template.pdf +0 -0
- parla/texmf/tex/latex/arxiv-style/template.tex +214 -0
- parla/tools/__init__.py +0 -0
- parla/tools/claims.py +124 -0
- parla/tools/formula.py +84 -0
- parla/tools/measure.py +72 -0
- parla/tools/pdf/__init__.py +39 -0
- parla/tools/pdf/base.py +28 -0
- parla/tools/pdf/heuristics.py +529 -0
- parla/tools/pdf/pdftotext.py +131 -0
- parla/tools/pdf/pymupdf.py +127 -0
- parla/tools/pdf/pymupdf4llm.py +112 -0
- parla/tools/sections.py +909 -0
- parla/tools/source.py +109 -0
- parla/tools/table.py +64 -0
- parla/tools/table_fit.py +181 -0
- parla/tools/table_parse.py +357 -0
- parla/tools/table_render.py +191 -0
- parla-0.0.1.dist-info/METADATA +94 -0
- parla-0.0.1.dist-info/RECORD +148 -0
- parla-0.0.1.dist-info/WHEEL +5 -0
- parla-0.0.1.dist-info/entry_points.txt +2 -0
- parla-0.0.1.dist-info/licenses/LICENSE +13 -0
- parla-0.0.1.dist-info/top_level.txt +1 -0
parla/__init__.py
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
|
parla/casebook.py
ADDED
|
@@ -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
|
parla/checks/__init__.py
ADDED
parla/checks/finding.py
ADDED
|
@@ -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"))
|
parla/checks/ids.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Every check id and its severity. Ids are stable: projects list them in `[checks].ignore`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .finding import Finding, Severity
|
|
6
|
+
|
|
7
|
+
SEVERITY: dict[str, Severity] = {
|
|
8
|
+
"card-invalid": "error",
|
|
9
|
+
"source-missing": "error",
|
|
10
|
+
"cite-unresolved": "error",
|
|
11
|
+
"figure-missing": "error",
|
|
12
|
+
"bib-name-syntax": "error",
|
|
13
|
+
"unescaped-char": "error",
|
|
14
|
+
"brace-unbalanced": "error",
|
|
15
|
+
"macro-missing": "error",
|
|
16
|
+
"translation-parity": "error",
|
|
17
|
+
"ref-dangling": "warning",
|
|
18
|
+
"float-unreferenced": "warning",
|
|
19
|
+
"float-order": "warning",
|
|
20
|
+
"figure-duplicate": "warning",
|
|
21
|
+
"abbreviation-repeated": "warning",
|
|
22
|
+
"cite-style": "warning",
|
|
23
|
+
"todo-marker": "warning",
|
|
24
|
+
"appendix-structure": "warning",
|
|
25
|
+
"svg-overflow": "error",
|
|
26
|
+
"svg-collision": "error",
|
|
27
|
+
"svg-arrow-crossing": "error",
|
|
28
|
+
"svg-glyph-missing": "error",
|
|
29
|
+
"svg-href-missing": "error",
|
|
30
|
+
"svg-malformed": "error",
|
|
31
|
+
"svg-math-in-text": "error",
|
|
32
|
+
"svg-unsupported": "warning",
|
|
33
|
+
"rule-malformed": "error",
|
|
34
|
+
"rule-origin-missing": "error",
|
|
35
|
+
"rule-duplicate": "error",
|
|
36
|
+
"rule-overrides-missing": "error",
|
|
37
|
+
"public-rule": "error",
|
|
38
|
+
"brief-missing": "error",
|
|
39
|
+
"link-broken": "error",
|
|
40
|
+
"guide-over-budget": "warning",
|
|
41
|
+
"guide-page-over-cap": "warning",
|
|
42
|
+
"hook-missing": "error",
|
|
43
|
+
"topic-applicable-unknown": "error",
|
|
44
|
+
"constraint-example-failed": "error",
|
|
45
|
+
"constraint-refine": "warning",
|
|
46
|
+
"frame-overflow": "error",
|
|
47
|
+
"overfull-line": "warning",
|
|
48
|
+
"rule-contradicted": "warning",
|
|
49
|
+
"case-distill": "warning",
|
|
50
|
+
"case-stale": "warning",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def finding(check_id: str, file: str, line: int, message: str) -> Finding:
|
|
55
|
+
return Finding(file, line, SEVERITY[check_id], check_id, message)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_constraint(f: Finding) -> bool:
|
|
59
|
+
return f.id.startswith("constraint/")
|
parla/checks/shared.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Checks every engine shares. See `specs/checks.md#ids`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .. import config as config_mod
|
|
9
|
+
from ..names import PROJECT_DIR
|
|
10
|
+
from .finding import Finding
|
|
11
|
+
from .ids import finding
|
|
12
|
+
|
|
13
|
+
TEXT_SUFFIXES = {".tex", ".md", ".svg", ".txt"}
|
|
14
|
+
SKIPPED_FILES = {"card.md", "references.bib"}
|
|
15
|
+
_FIGURE_EXTENSIONS = (".pdf", ".png", ".jpg", ".jpeg", ".eps", ".svg", ".tex")
|
|
16
|
+
_COMMENT_RE = re.compile(r"(?<!\\)%.*")
|
|
17
|
+
_SOURCE_PATH_RE = re.compile(r"(?<![\w/.\-])sources/[^\s{}()\[\],\"'<>]+")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def card_invalid(root: Path, artifact: str) -> Finding | None:
|
|
21
|
+
"""A finding when the artifact has a card that does not validate; `None` when there is no
|
|
22
|
+
card file (the caller fails on that) or the card is valid."""
|
|
23
|
+
if not config_mod.card_path(root, artifact).is_file():
|
|
24
|
+
return None
|
|
25
|
+
try:
|
|
26
|
+
config_mod.load_card(root, artifact)
|
|
27
|
+
except config_mod.ConfigError as exc:
|
|
28
|
+
return finding("card-invalid", f"artifacts/{artifact}/card.md", 1, str(exc))
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def artifact_files(root: Path, artifact: str) -> list[Path]:
|
|
33
|
+
base = root / PROJECT_DIR / "artifacts" / artifact
|
|
34
|
+
return [
|
|
35
|
+
p for p in sorted(base.rglob("*"))
|
|
36
|
+
if p.is_file() and p.suffix in TEXT_SUFFIXES and p.name not in SKIPPED_FILES
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _exists(base: Path, rel: str) -> bool:
|
|
41
|
+
path = base / rel
|
|
42
|
+
return path.exists() or any(path.with_name(path.name + ext).exists() for ext in _FIGURE_EXTENSIONS)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def source_missing(root: Path, artifact: str) -> list[Finding]:
|
|
46
|
+
"""Every `sources/...` path an artifact file names that does not exist under `parla/`."""
|
|
47
|
+
base = root / PROJECT_DIR
|
|
48
|
+
findings: list[Finding] = []
|
|
49
|
+
for path in artifact_files(root, artifact):
|
|
50
|
+
rel_file = path.relative_to(base).as_posix()
|
|
51
|
+
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
|
52
|
+
text = _COMMENT_RE.sub("", line) if path.suffix == ".tex" else line
|
|
53
|
+
for match in _SOURCE_PATH_RE.finditer(text):
|
|
54
|
+
target = match.group(0).rstrip(".:;")
|
|
55
|
+
if not _exists(base, target):
|
|
56
|
+
findings.append(finding("source-missing", rel_file, number, f"{target} does not exist"))
|
|
57
|
+
return findings
|