pgn-postmortem 0.1.0__tar.gz → 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/PKG-INFO +88 -3
  2. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/README.md +87 -2
  3. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem/__init__.py +45 -1
  4. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem/analysis.py +47 -4
  5. pgn_postmortem-2.0.0/pgn_postmortem/cli.py +304 -0
  6. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem/collection.py +46 -5
  7. pgn_postmortem-2.0.0/pgn_postmortem/results.py +446 -0
  8. pgn_postmortem-2.0.0/pgn_postmortem/selection.py +220 -0
  9. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem/site.py +216 -56
  10. pgn_postmortem-2.0.0/pgn_postmortem/workspace.py +373 -0
  11. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem.egg-info/PKG-INFO +88 -3
  12. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem.egg-info/SOURCES.txt +11 -1
  13. pgn_postmortem-2.0.0/tests/test_book_demo.py +69 -0
  14. pgn_postmortem-2.0.0/tests/test_demo.py +151 -0
  15. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_history.py +4 -1
  16. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_publish.py +45 -0
  17. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_quiz.py +10 -1
  18. pgn_postmortem-2.0.0/tests/test_result_correction.py +493 -0
  19. pgn_postmortem-2.0.0/tests/test_result_docs.py +105 -0
  20. pgn_postmortem-2.0.0/tests/test_result_policies.py +394 -0
  21. pgn_postmortem-2.0.0/tests/test_selection.py +104 -0
  22. pgn_postmortem-2.0.0/tests/test_workspace.py +210 -0
  23. pgn_postmortem-0.1.0/pgn_postmortem/cli.py +0 -159
  24. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/LICENSE +0 -0
  25. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem/__main__.py +0 -0
  26. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem/static/history.js +0 -0
  27. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem.egg-info/dependency_links.txt +0 -0
  28. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem.egg-info/entry_points.txt +0 -0
  29. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem.egg-info/requires.txt +0 -0
  30. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem.egg-info/top_level.txt +0 -0
  31. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pyproject.toml +0 -0
  32. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/setup.cfg +0 -0
  33. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_analysis.py +0 -0
  34. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_boards.py +0 -0
  35. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_cli.py +0 -0
  36. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_collection.py +0 -0
  37. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_library_analysis.py +0 -0
  38. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_lichess_links.py +0 -0
  39. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_openings.py +0 -0
  40. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_outcome_swings.py +0 -0
  41. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_pgn_io.py +0 -0
  42. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_presumed_results.py +0 -0
  43. {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_site.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgn-postmortem
3
- Version: 0.1.0
3
+ Version: 2.0.0
4
4
  Summary: Turn a folder of PGN chess games into browsable Markdown post-mortems of every blunder, analyzed locally with Stockfish.
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
@@ -22,8 +22,18 @@ should have been punished, and an opening-theory breakdown showing where the gam
22
22
  Everything runs locally with [Stockfish](https://stockfishchess.org/), and the output is plain Markdown
23
23
  and SVG. There's no account and no server, and the site lives in git.
24
24
 
25
- **[→ Live demo](https://diegoami.github.io/pgn-postmortem/)**: five classic games, from Chigorin–Steinitz
26
- (1892) to Carlsen–Anand (2014). The same pages are also [browsable on GitHub](examples/docs/index.md).
25
+ **[→ Live demo](https://diegoami.github.io/pgn-postmortem/)**: the library's Wikipedia-style site of five
26
+ classic games, from Chigorin–Steinitz (1892) to Carlsen–Anand (2014), with a "what would you play?"
27
+ question at each critical moment. The Markdown pages this README describes are at the demo's
28
+ [`/markdown/`](https://diegoami.github.io/pgn-postmortem/markdown/), and are also
29
+ [browsable on GitHub](examples/docs/index.md).
30
+
31
+ **[→ A real collection](https://diegoami.github.io/chessgamescollection/)**: the author's own games, built with
32
+ this library as three separate collections (source in
33
+ [diegoami/chessgamescollection](https://github.com/diegoami/chessgamescollection)):
34
+ [over-the-board games](https://diegoami.github.io/chessgamescollection/otb/) (148),
35
+ [correspondence games](https://diegoami.github.io/chessgamescollection/correspondence/) (1,068) and
36
+ [standard games](https://diegoami.github.io/chessgamescollection/standard/) (264, played online).
27
37
 
28
38
  <table>
29
39
  <tr>
@@ -176,6 +186,25 @@ analyzed.build_site("site/", title="Games of Ada Example") # the quiz is for th
176
186
 
177
187
  Quote a `**` pattern so the library, not the shell, expands it. The scripts above are unchanged by it.
178
188
 
189
+ ### F-1.3 book demo
190
+
191
+ `examples/book_demo/source/capablanca.pgn` is a five-game Capablanca subset from
192
+ [PGN Mentor's Capablanca collection](https://www.pgnmentor.com/players/Capablanca.zip),
193
+ listed on its [free PGN downloads page](https://www.pgnmentor.com/files.html). The files are
194
+ available for download completely free; the source page does not publish a separate open-source
195
+ license, so the README does not claim one. The checked-in subset is the 1906 Raubitschek game and
196
+ Marshall rounds 1, 2, 5 and 6 from 1909. The Stockfish 19, depth-22 files under
197
+ `examples/book_demo/analyzed/` are derived/analyzed data from that source. This repository's
198
+ distribution policy limits this demo data to non-commercial use. The originally selected Caissabase source remains documented in
199
+ `ROADMAP.md`; its archived page is readable but its 630 MB download was not captured by Wayback.
200
+
201
+ Build the separate local book demo from the committed analysis without running Stockfish:
202
+
203
+ ```bash
204
+ pgn-postmortem site examples/book_demo/analyzed --player "Capablanca, Jose Raul" \
205
+ --title "José Raúl Capablanca" --out examples/book_demo/site
206
+ ```
207
+
179
208
  Every game the library writes is named `<date>-<id>.pgn` and carries two headers of its own:
180
209
 
181
210
  - `PostmortemId`, the game's content id;
@@ -282,6 +311,62 @@ The site of the test fixture is committed in [`tests/golden/site/`](tests/golden
282
311
  the same site without the reading history in
283
312
  [`tests/golden/site-no-history/`](tests/golden/site-no-history/index.html).
284
313
 
314
+ ### Correcting recorded results
315
+
316
+ **Optional: nothing is corrected unless you ask.** Reading, `analyze`, `site` and `workspace` never touch
317
+ a result by default.
318
+
319
+ A source's `Result` is sometimes wrong, a residue of computer analysis: the side with the won position
320
+ recorded as the loser, a draw as a defeat. For games the library analyzed, `correct-results` reads the
321
+ final position (the board first: checkmate, stalemate, insufficient material; else the final `[%eval]`,
322
+ a win for a side with at least 70% winning chances, a draw otherwise) and writes the result it gives into
323
+ the game's `Result` header, keeping the source's value in an `OriginalResult` header. No engine runs.
324
+
325
+ **The caveat, honestly:** a decisive result in a level position can be genuine (a time forfeit, a
326
+ resignation, an adjudication), and the library cannot tell. So you choose how far it may go and which
327
+ games it must leave alone, every change is listed with its reason, and the original is always kept.
328
+
329
+ | policy | what it writes |
330
+ |---|---|
331
+ | `all` (default) | the final position's result whenever there is one: a recorded win in a level position becomes a draw |
332
+ | `contradictions` | only a recorded win or loss reversed, or a recorded draw made decisive; never a decisive result made a draw because the position is level, and `*` is left alone |
333
+ | `unrecorded` | only a missing or `*` result (the rule above, written to the file) |
334
+ | `board` | only what the board itself decides (checkmate, stalemate, insufficient material) |
335
+
336
+ ```console tested
337
+ $ pgn-postmortem correct-results analyzed --policy contradictions --dry-run --explain
338
+ Policy: contradictions, threshold 70.
339
+ 2013-01-01-30390dba02.pgn: 0-1 -> 1-0 (Ada Example vs. Bert Sample, corrected)
340
+ 2013-01-02-545b079385.pgn: kept: policy (1-0, the final position says 1/2-1/2)
341
+ ```
342
+
343
+ `--threshold` (55 to 95, one value) sets the winning chances that make a win, and `--skip-header
344
+ NAME=REGEX` (repeatable) leaves alone the games whose header matches, for example
345
+ `--skip-header 'Termination=(?i)forfeit|time'`. `--dry-run` lists and writes nothing; `--explain` also
346
+ says why each game was kept (`kept: agrees`, `kept: policy`, `kept: skipped`, `kept: no verdict`).
347
+ Running again changes nothing; a narrower policy restores what it would not correct. The game's id and
348
+ file name do not change (the id is computed from the source's result), so analysis is not redone and the
349
+ analyzed copy still matches its source. For new games, `pgn-postmortem analyze ... --correct-results`
350
+ (with `--result-policy`, `--result-threshold`, `--result-skip-header`) does it as each game is analyzed.
351
+ In Python, `correct_results("analyzed/", policy="contradictions", skip=...)`,
352
+ `Collection.correct_results(policy=...)` (in memory) and `analyze(..., correct_results=True)`. In a
353
+ workspace manifest, per collection, in memory only (no file is rewritten):
354
+
355
+ ```toml tested
356
+ [[collection]]
357
+ slug = "otb"
358
+ title = "Over-the-board games"
359
+ inputs = ["analyzed"]
360
+ correct_results = "contradictions"
361
+ ```
362
+
363
+ **For anyone scripting against the output:** `correct-results` and `analyze --correct-results` now
364
+ print a `Policy: NAME, threshold T.` line first (the change lines and the summary are as before).
365
+
366
+ An article of a corrected game shows a "Source result" row and says the source recorded something else.
367
+ The full reference, with a worked example of each policy, the skip rules, undoing a correction and the
368
+ library API, is in [`docs/result-correction.md`](docs/result-correction.md).
369
+
285
370
  ## Claude Code skill
286
371
 
287
372
  [`.claude/skills/publish-games`](.claude/skills/publish-games/SKILL.md) is a
@@ -11,8 +11,18 @@ should have been punished, and an opening-theory breakdown showing where the gam
11
11
  Everything runs locally with [Stockfish](https://stockfishchess.org/), and the output is plain Markdown
12
12
  and SVG. There's no account and no server, and the site lives in git.
13
13
 
14
- **[→ Live demo](https://diegoami.github.io/pgn-postmortem/)**: five classic games, from Chigorin–Steinitz
15
- (1892) to Carlsen–Anand (2014). The same pages are also [browsable on GitHub](examples/docs/index.md).
14
+ **[→ Live demo](https://diegoami.github.io/pgn-postmortem/)**: the library's Wikipedia-style site of five
15
+ classic games, from Chigorin–Steinitz (1892) to Carlsen–Anand (2014), with a "what would you play?"
16
+ question at each critical moment. The Markdown pages this README describes are at the demo's
17
+ [`/markdown/`](https://diegoami.github.io/pgn-postmortem/markdown/), and are also
18
+ [browsable on GitHub](examples/docs/index.md).
19
+
20
+ **[→ A real collection](https://diegoami.github.io/chessgamescollection/)**: the author's own games, built with
21
+ this library as three separate collections (source in
22
+ [diegoami/chessgamescollection](https://github.com/diegoami/chessgamescollection)):
23
+ [over-the-board games](https://diegoami.github.io/chessgamescollection/otb/) (148),
24
+ [correspondence games](https://diegoami.github.io/chessgamescollection/correspondence/) (1,068) and
25
+ [standard games](https://diegoami.github.io/chessgamescollection/standard/) (264, played online).
16
26
 
17
27
  <table>
18
28
  <tr>
@@ -165,6 +175,25 @@ analyzed.build_site("site/", title="Games of Ada Example") # the quiz is for th
165
175
 
166
176
  Quote a `**` pattern so the library, not the shell, expands it. The scripts above are unchanged by it.
167
177
 
178
+ ### F-1.3 book demo
179
+
180
+ `examples/book_demo/source/capablanca.pgn` is a five-game Capablanca subset from
181
+ [PGN Mentor's Capablanca collection](https://www.pgnmentor.com/players/Capablanca.zip),
182
+ listed on its [free PGN downloads page](https://www.pgnmentor.com/files.html). The files are
183
+ available for download completely free; the source page does not publish a separate open-source
184
+ license, so the README does not claim one. The checked-in subset is the 1906 Raubitschek game and
185
+ Marshall rounds 1, 2, 5 and 6 from 1909. The Stockfish 19, depth-22 files under
186
+ `examples/book_demo/analyzed/` are derived/analyzed data from that source. This repository's
187
+ distribution policy limits this demo data to non-commercial use. The originally selected Caissabase source remains documented in
188
+ `ROADMAP.md`; its archived page is readable but its 630 MB download was not captured by Wayback.
189
+
190
+ Build the separate local book demo from the committed analysis without running Stockfish:
191
+
192
+ ```bash
193
+ pgn-postmortem site examples/book_demo/analyzed --player "Capablanca, Jose Raul" \
194
+ --title "José Raúl Capablanca" --out examples/book_demo/site
195
+ ```
196
+
168
197
  Every game the library writes is named `<date>-<id>.pgn` and carries two headers of its own:
169
198
 
170
199
  - `PostmortemId`, the game's content id;
@@ -271,6 +300,62 @@ The site of the test fixture is committed in [`tests/golden/site/`](tests/golden
271
300
  the same site without the reading history in
272
301
  [`tests/golden/site-no-history/`](tests/golden/site-no-history/index.html).
273
302
 
303
+ ### Correcting recorded results
304
+
305
+ **Optional: nothing is corrected unless you ask.** Reading, `analyze`, `site` and `workspace` never touch
306
+ a result by default.
307
+
308
+ A source's `Result` is sometimes wrong, a residue of computer analysis: the side with the won position
309
+ recorded as the loser, a draw as a defeat. For games the library analyzed, `correct-results` reads the
310
+ final position (the board first: checkmate, stalemate, insufficient material; else the final `[%eval]`,
311
+ a win for a side with at least 70% winning chances, a draw otherwise) and writes the result it gives into
312
+ the game's `Result` header, keeping the source's value in an `OriginalResult` header. No engine runs.
313
+
314
+ **The caveat, honestly:** a decisive result in a level position can be genuine (a time forfeit, a
315
+ resignation, an adjudication), and the library cannot tell. So you choose how far it may go and which
316
+ games it must leave alone, every change is listed with its reason, and the original is always kept.
317
+
318
+ | policy | what it writes |
319
+ |---|---|
320
+ | `all` (default) | the final position's result whenever there is one: a recorded win in a level position becomes a draw |
321
+ | `contradictions` | only a recorded win or loss reversed, or a recorded draw made decisive; never a decisive result made a draw because the position is level, and `*` is left alone |
322
+ | `unrecorded` | only a missing or `*` result (the rule above, written to the file) |
323
+ | `board` | only what the board itself decides (checkmate, stalemate, insufficient material) |
324
+
325
+ ```console tested
326
+ $ pgn-postmortem correct-results analyzed --policy contradictions --dry-run --explain
327
+ Policy: contradictions, threshold 70.
328
+ 2013-01-01-30390dba02.pgn: 0-1 -> 1-0 (Ada Example vs. Bert Sample, corrected)
329
+ 2013-01-02-545b079385.pgn: kept: policy (1-0, the final position says 1/2-1/2)
330
+ ```
331
+
332
+ `--threshold` (55 to 95, one value) sets the winning chances that make a win, and `--skip-header
333
+ NAME=REGEX` (repeatable) leaves alone the games whose header matches, for example
334
+ `--skip-header 'Termination=(?i)forfeit|time'`. `--dry-run` lists and writes nothing; `--explain` also
335
+ says why each game was kept (`kept: agrees`, `kept: policy`, `kept: skipped`, `kept: no verdict`).
336
+ Running again changes nothing; a narrower policy restores what it would not correct. The game's id and
337
+ file name do not change (the id is computed from the source's result), so analysis is not redone and the
338
+ analyzed copy still matches its source. For new games, `pgn-postmortem analyze ... --correct-results`
339
+ (with `--result-policy`, `--result-threshold`, `--result-skip-header`) does it as each game is analyzed.
340
+ In Python, `correct_results("analyzed/", policy="contradictions", skip=...)`,
341
+ `Collection.correct_results(policy=...)` (in memory) and `analyze(..., correct_results=True)`. In a
342
+ workspace manifest, per collection, in memory only (no file is rewritten):
343
+
344
+ ```toml tested
345
+ [[collection]]
346
+ slug = "otb"
347
+ title = "Over-the-board games"
348
+ inputs = ["analyzed"]
349
+ correct_results = "contradictions"
350
+ ```
351
+
352
+ **For anyone scripting against the output:** `correct-results` and `analyze --correct-results` now
353
+ print a `Policy: NAME, threshold T.` line first (the change lines and the summary are as before).
354
+
355
+ An article of a corrected game shows a "Source result" row and says the source recorded something else.
356
+ The full reference, with a worked example of each policy, the skip rules, undoing a correction and the
357
+ library API, is in [`docs/result-correction.md`](docs/result-correction.md).
358
+
274
359
  ## Claude Code skill
275
360
 
276
361
  [`.claude/skills/publish-games`](.claude/skills/publish-games/SKILL.md) is a
@@ -18,16 +18,54 @@ The same steps from the command line are ``pgn-postmortem read``,
18
18
 
19
19
  from pgn_postmortem.analysis import AnalysisReport, EngineFailure, Thresholds, analyze_games
20
20
  from pgn_postmortem.collection import CollectedGame, Collection, ReadReport, find_pgn_files
21
+ from pgn_postmortem.results import (
22
+ POLICIES,
23
+ CorrectionReport,
24
+ GameDecision,
25
+ ResultChange,
26
+ check_policy,
27
+ correct_game,
28
+ correct_results,
29
+ decided_result,
30
+ header_skip,
31
+ )
32
+ from pgn_postmortem.selection import (
33
+ ChapterWeights,
34
+ SelectionFeatures,
35
+ SelectionOptions,
36
+ SelectionWeights,
37
+ select_chapters,
38
+ )
21
39
  from pgn_postmortem.site import SiteReport, build_site, critical_moments, shown_result
40
+ from pgn_postmortem.workspace import (
41
+ CollectionProfile,
42
+ Workspace,
43
+ WorkspaceBuildError,
44
+ WorkspaceConfigError,
45
+ WorkspaceReport,
46
+ )
22
47
 
23
- __version__ = "0.1.0"
48
+ __version__ = "2.0.0"
24
49
 
25
50
  __all__ = [
26
51
  "AnalysisReport",
27
52
  "CollectedGame",
28
53
  "Collection",
54
+ "CorrectionReport",
55
+ "ResultChange",
56
+ "GameDecision",
57
+ "POLICIES",
58
+ "check_policy",
59
+ "header_skip",
60
+ "correct_game",
61
+ "correct_results",
62
+ "decided_result",
29
63
  "EngineFailure",
30
64
  "ReadReport",
65
+ "ChapterWeights",
66
+ "SelectionFeatures",
67
+ "SelectionOptions",
68
+ "SelectionWeights",
31
69
  "Thresholds",
32
70
  "__version__",
33
71
  "SiteReport",
@@ -36,4 +74,10 @@ __all__ = [
36
74
  "critical_moments",
37
75
  "find_pgn_files",
38
76
  "shown_result",
77
+ "select_chapters",
78
+ "CollectionProfile",
79
+ "Workspace",
80
+ "WorkspaceBuildError",
81
+ "WorkspaceConfigError",
82
+ "WorkspaceReport",
39
83
  ]
@@ -45,7 +45,7 @@ from collections.abc import Callable, Iterable
45
45
  from concurrent.futures import ThreadPoolExecutor, as_completed
46
46
  from dataclasses import dataclass, field
47
47
  from pathlib import Path
48
- from typing import NamedTuple
48
+ from typing import TYPE_CHECKING, NamedTuple
49
49
 
50
50
  import chess
51
51
  import chess.engine
@@ -53,6 +53,9 @@ import chess.pgn
53
53
 
54
54
  from pgn_postmortem.collection import ANALYSIS_HEADER, CollectedGame, analyzed_ids, format_game
55
55
 
56
+ if TYPE_CHECKING:
57
+ from pgn_postmortem.results import ResultChange
58
+
56
59
  FALLBACK_ENGINE_PATH = "/usr/games/stockfish" # where Debian and Ubuntu install it, off the default PATH
57
60
 
58
61
  MATE_SCORE = 100000
@@ -86,6 +89,7 @@ class AnalysisReport:
86
89
  analyzed: int = 0
87
90
  skipped: int = 0
88
91
  written: list[Path] = field(default_factory=list)
92
+ corrections: list[ResultChange] = field(default_factory=list) # with ``correct_results``: each result rewritten
89
93
 
90
94
 
91
95
  def default_engine_path() -> str:
@@ -216,6 +220,10 @@ def analyze_games(
216
220
  engine_path: str | None = None,
217
221
  pv_plies: int = DEFAULT_PV_PLIES,
218
222
  thresholds: Thresholds = LICHESS_THRESHOLDS,
223
+ correct_results: bool = False,
224
+ presume_threshold: float | None = None,
225
+ result_policy: str | None = None,
226
+ result_skip: Callable[[chess.pgn.Game], bool] | None = None,
219
227
  progress: Callable[[int, int, CollectedGame], None] | None = None,
220
228
  ) -> AnalysisReport:
221
229
  """Analyze every game not yet in ``out_dir`` into ``out_dir/<date>-<id>.pgn``.
@@ -225,7 +233,35 @@ def analyze_games(
225
233
  processes run side by side (0: one per CPU). No engine is started when
226
234
  there is nothing to analyze. ``progress(done, total, game)`` is called
227
235
  after each game.
236
+
237
+ With ``correct_results``, each game analyzed now has its ``Result``
238
+ corrected from its final position before it is written
239
+ (``pgn_postmortem.results``, ROADMAP.md F-14; ``presume_threshold`` is the
240
+ winning chances in percent, 70 by default, 55 to 95, checked before
241
+ anything is written or any engine started). ``result_policy`` (one of
242
+ ``pgn_postmortem.results.POLICIES``, ``all`` by default) says which
243
+ corrections are made and ``result_skip(game) -> bool`` leaves a game's
244
+ result as it is (see ``pgn_postmortem.results``); giving either without
245
+ ``correct_results`` raises ``ValueError``. A game already analyzed
246
+ into ``out_dir`` is not touched: ``pgn_postmortem.results.correct_results``
247
+ corrects those without an engine.
228
248
  """
249
+ from pgn_postmortem.results import (
250
+ DEFAULT_POLICY,
251
+ PRESUME_THRESHOLD,
252
+ check_policy,
253
+ check_presume_threshold,
254
+ correct_game,
255
+ )
256
+
257
+ result_threshold = PRESUME_THRESHOLD if presume_threshold is None else presume_threshold
258
+ if correct_results or presume_threshold is not None:
259
+ check_presume_threshold(result_threshold)
260
+ if result_policy is not None:
261
+ check_policy(result_policy)
262
+ if not correct_results and (result_policy is not None or result_skip is not None):
263
+ raise ValueError("result_policy and result_skip only apply with correct_results=True")
264
+ policy = result_policy or DEFAULT_POLICY
229
265
  out_dir = Path(out_dir)
230
266
  out_dir.mkdir(parents=True, exist_ok=True)
231
267
  done_ids = analyzed_ids(out_dir)
@@ -239,7 +275,7 @@ def analyze_games(
239
275
  workers = max(1, min(workers or os.cpu_count() or 1, len(todo)))
240
276
  engines: queue.Queue[chess.engine.SimpleEngine] = queue.Queue()
241
277
 
242
- def run(item: CollectedGame) -> Path:
278
+ def run(item: CollectedGame) -> tuple[Path, ResultChange | None]:
243
279
  engine = engines.get()
244
280
  try:
245
281
  analyzed = analyze_game(engine, limit, item.game, item.id, pv_plies, thresholds)
@@ -247,11 +283,14 @@ def analyze_games(
247
283
  raise EngineFailure(f"the engine failed on {item.origin}: {err}") from err
248
284
  finally:
249
285
  engines.put(engine)
286
+ change = None
287
+ if correct_results:
288
+ change = correct_game(analyzed, result_threshold, item.filename, policy=policy, skip=result_skip)
250
289
  path = out_dir / item.filename
251
290
  partial = path.with_name(path.name + ".partial")
252
291
  partial.write_text(format_game(analyzed), encoding="utf-8")
253
292
  os.replace(partial, path) # a run that is interrupted never leaves a half-written game behind
254
- return path
293
+ return path, change
255
294
 
256
295
  engine_path = engine_path or default_engine_path()
257
296
  try:
@@ -269,7 +308,10 @@ def analyze_games(
269
308
  futures = {pool.submit(run, item): item for item in todo}
270
309
  try:
271
310
  for done, future in enumerate(as_completed(futures), start=1):
272
- report.written.append(future.result())
311
+ path, change = future.result()
312
+ report.written.append(path)
313
+ if change:
314
+ report.corrections.append(change)
273
315
  report.analyzed += 1
274
316
  if progress:
275
317
  progress(done, len(todo), futures[future])
@@ -285,4 +327,5 @@ def analyze_games(
285
327
  except ENGINE_ERRORS:
286
328
  pass # it already died
287
329
  report.written.sort()
330
+ report.corrections.sort(key=lambda change: change.path)
288
331
  return report