evidome 1.0__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.
evidome/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """Functional evidence reports for an interpreting agent.
2
+
3
+ The program computes; an agent interprets. It makes no interpretive claims of
4
+ its own.
5
+ """
6
+
7
+ from .version import __version__
8
+
9
+ __all__ = ["__version__"]
evidome/checkpoint.py ADDED
@@ -0,0 +1,111 @@
1
+ """"Is this stage already done?" -- answered here, and only here.
2
+
3
+ This module exists because of Paralome's most expensive recurring bug. There,
4
+ "is this stage done" was answered in four independent places (the stage's own
5
+ checkpoint check, the fan-out wrapper, the index payload filter, and the
6
+ orchestrator/supervisor) that never consulted each other. A fix at one layer
7
+ left the other three wrong, so fixes shipped one layer at a time, each
8
+ "verified", each insufficient.
9
+
10
+ The rule here: every caller that needs to know whether a stage can be skipped
11
+ -- the stage itself, any runner, any status command, any future supervisor --
12
+ calls ``reuse_decision``. Nobody reads ``complete: true`` out of a checkpoint
13
+ and draws their own conclusion.
14
+
15
+ ``reuse_decision`` checks three things, in this order, and returns the FIRST
16
+ reason to recompute as a sentence (never a bare bool -- "recomputing because X"
17
+ is actionable; "recomputing" is not):
18
+
19
+ 1. version -- a checkpoint written by any other program version is stale.
20
+ Safe because the version only bumps on a behaviour change.
21
+ 2. parameters -- every parameter that affects the output must match.
22
+ 3. outputs -- every recorded output file must still exist. Deleting results
23
+ and re-running is the routine case, not the edge case.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from dataclasses import dataclass
29
+ from pathlib import Path
30
+ from typing import Any, Dict, Iterable, Mapping, Optional
31
+
32
+ from .jsonio import VERSION_KEY, read_json, write_json
33
+ from .version import __version__
34
+
35
+ COMPLETE_KEY = "complete"
36
+ PARAMS_KEY = "parameters"
37
+ OUTPUTS_KEY = "outputs"
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class ReuseDecision:
42
+ reuse: bool
43
+ reason: str
44
+ checkpoint: Dict[str, Any]
45
+
46
+ def __bool__(self) -> bool: # `if decision:` reads as "can reuse"
47
+ return self.reuse
48
+
49
+
50
+ def reuse_decision(
51
+ checkpoint_path: Path,
52
+ parameters: Mapping[str, Any],
53
+ *,
54
+ version: Optional[str] = None,
55
+ ) -> ReuseDecision:
56
+ """Whether the checkpoint at ``checkpoint_path`` may stand in for a re-run."""
57
+ path = Path(checkpoint_path)
58
+ if not path.exists():
59
+ return ReuseDecision(False, f"no checkpoint at {path}", {})
60
+ previous = read_json(path)
61
+ if not isinstance(previous, dict): # valid JSON, but not a checkpoint
62
+ return ReuseDecision(False, f"{path} is not a checkpoint object", {})
63
+ if not previous.get(COMPLETE_KEY):
64
+ return ReuseDecision(False, "checkpoint is not marked complete", previous)
65
+
66
+ running = version if version is not None else __version__
67
+ recorded = previous.get(VERSION_KEY)
68
+ if recorded != running:
69
+ return ReuseDecision(False, f"written by version {recorded}, running {running}", previous)
70
+
71
+ recorded_params = previous.get(PARAMS_KEY, {})
72
+ for key in sorted(set(parameters) | set(recorded_params)):
73
+ if _normalise(parameters.get(key)) != _normalise(recorded_params.get(key)):
74
+ return ReuseDecision(
75
+ False,
76
+ f"parameter {key!r} changed: {recorded_params.get(key)!r} -> {parameters.get(key)!r}",
77
+ previous,
78
+ )
79
+
80
+ for name, output in sorted(previous.get(OUTPUTS_KEY, {}).items()):
81
+ if output and not Path(output).exists():
82
+ return ReuseDecision(False, f"recorded output {name!r} no longer exists: {output}", previous)
83
+
84
+ return ReuseDecision(True, "version, parameters and outputs all match", previous)
85
+
86
+
87
+ def write_checkpoint(
88
+ checkpoint_path: Path,
89
+ parameters: Mapping[str, Any],
90
+ outputs: Mapping[str, Optional[str]],
91
+ **extra: Any,
92
+ ) -> Path:
93
+ """Record a completed stage. ``outputs`` maps output kind -> path."""
94
+ payload: Dict[str, Any] = dict(extra)
95
+ payload[COMPLETE_KEY] = True
96
+ payload[PARAMS_KEY] = {k: _normalise(v) for k, v in parameters.items()}
97
+ payload[OUTPUTS_KEY] = {k: (str(v) if v is not None else None) for k, v in outputs.items()}
98
+ return write_json(checkpoint_path, payload)
99
+
100
+
101
+ def _normalise(value: Any) -> Any:
102
+ """Make recorded and requested parameters comparable after a JSON round trip."""
103
+ if isinstance(value, Path):
104
+ return str(value)
105
+ if isinstance(value, tuple):
106
+ return [_normalise(v) for v in value]
107
+ if isinstance(value, list):
108
+ return [_normalise(v) for v in value]
109
+ if isinstance(value, Mapping):
110
+ return {str(k): _normalise(v) for k, v in value.items()}
111
+ return value