puremethod 0.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.
- app/__init__.py +2 -0
- app/__main__.py +31 -0
- app/_web/404/index.html +1 -0
- app/_web/404.html +1 -0
- app/_web/__next.!KGNvbnNvbGUp.__PAGE__.txt +42 -0
- app/_web/__next._full.txt +36 -0
- app/_web/__next._tree.txt +4 -0
- app/_web/_next/static/6az3RZxafySyERvJdZAgV/_buildManifest.js +11 -0
- app/_web/_next/static/6az3RZxafySyERvJdZAgV/_clientMiddlewareManifest.js +1 -0
- app/_web/_next/static/6az3RZxafySyERvJdZAgV/_ssgManifest.js +1 -0
- app/_web/_next/static/chunks/00fylloju-1x2.js +1 -0
- app/_web/_next/static/chunks/03hxwfej4sm9g.js +1 -0
- app/_web/_next/static/chunks/0cz1d0mv5g_q7.js +1 -0
- app/_web/_next/static/chunks/0gus9jro9n3j3.js +1 -0
- app/_web/_next/static/chunks/0n12a63ieedt9.js +1 -0
- app/_web/_next/static/chunks/0ns7paypbayry.js +31 -0
- app/_web/_next/static/chunks/0s1d7y-_wn8wn.js +1 -0
- app/_web/_next/static/chunks/12s40azz56b3b.js +1 -0
- app/_web/_next/static/chunks/1b_14wnek5geq.js +1 -0
- app/_web/_next/static/chunks/1oderptqhsaox.css +1 -0
- app/_web/_next/static/chunks/1rcnm-dleh7p6.js +1 -0
- app/_web/_next/static/chunks/1u4klctqx_w0w.js +1 -0
- app/_web/_next/static/chunks/1vahkxwiu_bqp.js +22 -0
- app/_web/_next/static/chunks/26mp4u1wew2xu.js +1 -0
- app/_web/_next/static/chunks/2aq880yo-2z88.js +1 -0
- app/_web/_next/static/chunks/2f5ovhdilzkal.js +1 -0
- app/_web/_next/static/chunks/2qds0qd18u74f.js +1 -0
- app/_web/_next/static/chunks/2qgmxvbiean_r.js +1 -0
- app/_web/_next/static/chunks/3b5v1mle2gmac.js +1 -0
- app/_web/_next/static/chunks/3hw8x3h98ysh4.js +6 -0
- app/_web/_next/static/chunks/3op5__grw4752.js +1 -0
- app/_web/_next/static/chunks/3scuzzds0vtp7.js +1 -0
- app/_web/_next/static/chunks/3v_5wdpvvcxz8.js +1 -0
- app/_web/_next/static/chunks/3wevuf_oyh97l.js +1 -0
- app/_web/_next/static/chunks/turbopack-3-ry682tkyiqk.js +1 -0
- app/_web/_not-found/__next._full.txt +24 -0
- app/_web/_not-found/__next._not-found.__PAGE__.txt +30 -0
- app/_web/_not-found/__next._tree.txt +4 -0
- app/_web/_not-found/index.html +1 -0
- app/_web/_not-found/index.txt +24 -0
- app/_web/credentials/__next.!KGNvbnNvbGUp.credentials.__PAGE__.txt +46 -0
- app/_web/credentials/__next._full.txt +39 -0
- app/_web/credentials/__next._tree.txt +5 -0
- app/_web/credentials/index.html +1 -0
- app/_web/credentials/index.txt +39 -0
- app/_web/executors/__next.!KGNvbnNvbGUp.executors.__PAGE__.txt +46 -0
- app/_web/executors/__next._full.txt +39 -0
- app/_web/executors/__next._tree.txt +5 -0
- app/_web/executors/index.html +1 -0
- app/_web/executors/index.txt +39 -0
- app/_web/index.html +1 -0
- app/_web/index.txt +36 -0
- app/_web/new/__next.!KGNvbnNvbGUp.new.__PAGE__.txt +46 -0
- app/_web/new/__next._full.txt +39 -0
- app/_web/new/__next._tree.txt +5 -0
- app/_web/new/index.html +1 -0
- app/_web/new/index.txt +39 -0
- app/_web/profiles/__next.!KGNvbnNvbGUp.profiles.__PAGE__.txt +46 -0
- app/_web/profiles/__next._full.txt +39 -0
- app/_web/profiles/__next._tree.txt +5 -0
- app/_web/profiles/index.html +1 -0
- app/_web/profiles/index.txt +39 -0
- app/_web/run/__next.!KGNvbnNvbGUp.run.__PAGE__.txt +47 -0
- app/_web/run/__next._full.txt +40 -0
- app/_web/run/__next._tree.txt +5 -0
- app/_web/run/index.html +1 -0
- app/_web/run/index.txt +40 -0
- app/_web/services/__next.!KGNvbnNvbGUp.services.__PAGE__.txt +46 -0
- app/_web/services/__next._full.txt +39 -0
- app/_web/services/__next._tree.txt +5 -0
- app/_web/services/index.html +1 -0
- app/_web/services/index.txt +39 -0
- app/_web/setup/__next.!KGNvbnNvbGUp.setup.__PAGE__.txt +46 -0
- app/_web/setup/__next._full.txt +39 -0
- app/_web/setup/__next._tree.txt +5 -0
- app/_web/setup/index.html +1 -0
- app/_web/setup/index.txt +39 -0
- app/_web/signin/__next._full.txt +30 -0
- app/_web/signin/__next._tree.txt +4 -0
- app/_web/signin/__next.signin.__PAGE__.txt +36 -0
- app/_web/signin/index.html +1 -0
- app/_web/signin/index.txt +30 -0
- app/accounts.py +137 -0
- app/addresses.py +30 -0
- app/api/__init__.py +61 -0
- app/api/auth_routes.py +54 -0
- app/api/common.py +102 -0
- app/api/config_routes.py +562 -0
- app/api/links.py +22 -0
- app/api/run_routes.py +311 -0
- app/api/setup_routes.py +267 -0
- app/bundled.py +521 -0
- app/compose.py +229 -0
- app/db.py +186 -0
- app/executor_config.py +95 -0
- app/keys.py +29 -0
- app/launcher.py +389 -0
- app/leases.py +40 -0
- app/literature_sources.py +131 -0
- app/model_endpoints.py +104 -0
- app/model_search.py +252 -0
- app/outputs.py +77 -0
- app/paths.py +93 -0
- app/readonly.py +70 -0
- app/repo.py +746 -0
- app/scan.py +65 -0
- app/server.py +24 -0
- app/settings.py +125 -0
- app/setup.py +117 -0
- app/ssh_chain.py +102 -0
- app/ssh_hosts.py +244 -0
- app/ssh_machines.py +101 -0
- app/statuses.py +4 -0
- app/supervisor.py +87 -0
- app/tunnels.py +222 -0
- app/unseal.py +27 -0
- app/worker.py +657 -0
- executor/__init__.py +47 -0
- executor/base.py +161 -0
- executor/common.py +139 -0
- executor/local_docker.py +239 -0
- executor/local_process.py +247 -0
- executor/ssh.py +481 -0
- executor/types.py +128 -0
- executor/watchdog.py +108 -0
- ledger/__init__.py +46 -0
- ledger/artifacts.py +62 -0
- ledger/canonical.py +48 -0
- ledger/core.py +337 -0
- ledger/errors.py +36 -0
- ledger/model.py +107 -0
- ledger/payloads.py +228 -0
- ledger/rules.py +204 -0
- ledger/storage.py +226 -0
- llm/__init__.py +45 -0
- llm/backends/__init__.py +0 -0
- llm/backends/anthropic_backend.py +145 -0
- llm/backends/base.py +87 -0
- llm/backends/openai_backend.py +211 -0
- llm/core.py +416 -0
- llm/prices.py +98 -0
- llm/tokens.py +18 -0
- llm/types.py +106 -0
- loop/__init__.py +19 -0
- loop/config.py +108 -0
- loop/core.py +1160 -0
- loop/decide.py +61 -0
- loop/harness.py +132 -0
- loop/prompts.py +88 -0
- loop/view.py +202 -0
- puremethod-0.1.0.dist-info/METADATA +79 -0
- puremethod-0.1.0.dist-info/RECORD +171 -0
- puremethod-0.1.0.dist-info/WHEEL +4 -0
- puremethod-0.1.0.dist-info/entry_points.txt +2 -0
- puremethod-0.1.0.dist-info/licenses/LICENSE +96 -0
- puremethod-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +173 -0
- purescholar/__init__.py +43 -0
- purescholar/client.py +380 -0
- purescholar/types.py +164 -0
- puretrace/__init__.py +49 -0
- puretrace/client.py +320 -0
- puretrace/types.py +166 -0
- writer/__init__.py +8 -0
- writer/core.py +430 -0
- writer/facts.py +162 -0
- writer/latex.py +62 -0
- writer/lint.py +85 -0
- writer/macros.py +114 -0
- writer/render.py +120 -0
- writer/tectonic.py +79 -0
- writer/types.py +61 -0
ledger/model.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Entries, derived views and reports (04-ledger.md §3, §4)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
Actor = Literal["loop", "user", "system"]
|
|
9
|
+
|
|
10
|
+
ENTRY_TYPES: tuple[str, ...] = (
|
|
11
|
+
"run.created",
|
|
12
|
+
"run.status",
|
|
13
|
+
"checkpoint.requested",
|
|
14
|
+
"checkpoint.decided",
|
|
15
|
+
"call.recorded",
|
|
16
|
+
"idea.proposed",
|
|
17
|
+
"hypothesis.proposed",
|
|
18
|
+
"hypothesis.novelty",
|
|
19
|
+
"screening.result",
|
|
20
|
+
"prereg.registered",
|
|
21
|
+
"prereg.amended",
|
|
22
|
+
"experiment.started",
|
|
23
|
+
"experiment.finished",
|
|
24
|
+
"decision.made",
|
|
25
|
+
"selfcheck.result",
|
|
26
|
+
"paper.built",
|
|
27
|
+
"correction",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
ARTIFACT_KINDS: tuple[str, ...] = (
|
|
31
|
+
"code", "env", "stdout", "stderr", "result", "figure", "paper", "prompt", "response",
|
|
32
|
+
"other",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
CONFIRM_STAGE = "confirm"
|
|
36
|
+
SMOKE_STAGE = "smoke" # harness checks; no decision uses them (INV-9, KB-018)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class Entry:
|
|
41
|
+
run_id: str
|
|
42
|
+
seq: int
|
|
43
|
+
entry_id: str
|
|
44
|
+
type: str
|
|
45
|
+
at: str
|
|
46
|
+
actor: Actor
|
|
47
|
+
payload: dict[str, Any]
|
|
48
|
+
refs: list[str]
|
|
49
|
+
prev_hash: str
|
|
50
|
+
hash: str
|
|
51
|
+
|
|
52
|
+
def hashed_fields(self) -> dict[str, Any]:
|
|
53
|
+
return {
|
|
54
|
+
"run_id": self.run_id,
|
|
55
|
+
"seq": self.seq,
|
|
56
|
+
"entry_id": self.entry_id,
|
|
57
|
+
"type": self.type,
|
|
58
|
+
"at": self.at,
|
|
59
|
+
"actor": self.actor,
|
|
60
|
+
"payload": self.payload,
|
|
61
|
+
"refs": self.refs,
|
|
62
|
+
"prev_hash": self.prev_hash,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
def to_dict(self) -> dict[str, Any]:
|
|
66
|
+
return {**self.hashed_fields(), "hash": self.hash}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class Rejection:
|
|
71
|
+
run_id: str
|
|
72
|
+
at: str
|
|
73
|
+
target: str # entry type, or "artifact"
|
|
74
|
+
rule: str
|
|
75
|
+
message: str
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class RunState:
|
|
80
|
+
"""Derived view: a pure function of the entries (04-ledger.md §5 step 2)."""
|
|
81
|
+
|
|
82
|
+
run_id: str
|
|
83
|
+
status: str | None = None
|
|
84
|
+
hypotheses: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
85
|
+
prereg_in_force: dict[tuple[str, str], str] = field(default_factory=dict)
|
|
86
|
+
open_experiments: list[str] = field(default_factory=list)
|
|
87
|
+
finished: dict[str, str] = field(default_factory=dict)
|
|
88
|
+
decisions: list[str] = field(default_factory=list)
|
|
89
|
+
last_seq: int = 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True)
|
|
93
|
+
class SpendSummary:
|
|
94
|
+
total_usd: float
|
|
95
|
+
by_source: dict[str, float]
|
|
96
|
+
unmetered_items: int
|
|
97
|
+
money_limit_usd: float | None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class ChainReport:
|
|
102
|
+
run_id: str
|
|
103
|
+
entries: int
|
|
104
|
+
ok: bool
|
|
105
|
+
first_break_seq: int | None = None
|
|
106
|
+
reason: str | None = None
|
|
107
|
+
head_hash: str | None = None
|
ledger/payloads.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Payload schemas per entry type (04-ledger.md §4, "Entry types (v1)").
|
|
2
|
+
|
|
3
|
+
The KB lists the main fields of each type; extra fields are allowed. Fields that hold entry
|
|
4
|
+
ids are checked by the rules in `rules.py`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
|
12
|
+
|
|
13
|
+
from .errors import LedgerRejection
|
|
14
|
+
|
|
15
|
+
NonEmpty = Field(min_length=1)
|
|
16
|
+
|
|
17
|
+
REASON_CODES = (
|
|
18
|
+
"refuted", "harness_error", "not_novel", "infeasible", "no_signal", "superseded",
|
|
19
|
+
"user_stopped", "limit_reached",
|
|
20
|
+
)
|
|
21
|
+
ReasonCode = Literal[
|
|
22
|
+
"refuted", "harness_error", "not_novel", "infeasible", "no_signal", "superseded",
|
|
23
|
+
"user_stopped", "limit_reached",
|
|
24
|
+
]
|
|
25
|
+
DataRole = Literal["selection", "held_out"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _Payload(BaseModel):
|
|
29
|
+
model_config = ConfigDict(extra="allow", strict=True)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RunCreated(_Payload):
|
|
33
|
+
idea: str = NonEmpty
|
|
34
|
+
config: dict[str, Any]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class RunStatus(_Payload):
|
|
38
|
+
status: Literal["preflight", "running", "paused", "stopping", "writing", "delivered",
|
|
39
|
+
"failed"]
|
|
40
|
+
reason: str = ""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class CheckpointRequested(_Payload):
|
|
44
|
+
checkpoint_id: str = NonEmpty
|
|
45
|
+
options: Any = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CheckpointDecided(_Payload):
|
|
49
|
+
checkpoint_id: str = NonEmpty
|
|
50
|
+
decision: Any
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CallRecorded(_Payload):
|
|
54
|
+
source: Literal["llm", "purescholar", "puretrace"]
|
|
55
|
+
record: dict[str, Any]
|
|
56
|
+
cost_usd: float | None
|
|
57
|
+
metered: bool
|
|
58
|
+
request_artifact: str | None = None
|
|
59
|
+
response_artifact: str | None = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class IdeaProposed(_Payload):
|
|
63
|
+
direction: str = NonEmpty
|
|
64
|
+
parents: list[str] = []
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class HypothesisProposed(_Payload):
|
|
68
|
+
statement: str = NonEmpty
|
|
69
|
+
idea: str = NonEmpty
|
|
70
|
+
kind: Literal["treatment", "baseline"]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class HypothesisNovelty(_Payload):
|
|
74
|
+
hypothesis: str = NonEmpty
|
|
75
|
+
evidence: list[str]
|
|
76
|
+
judgement: Literal["novel", "overlap", "unclear"]
|
|
77
|
+
reason: str = NonEmpty
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ScreeningResult(_Payload):
|
|
81
|
+
paper: Any
|
|
82
|
+
assessment: dict[str, Any]
|
|
83
|
+
decision: str = NonEmpty
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Metric(_Payload):
|
|
87
|
+
name: str = NonEmpty
|
|
88
|
+
unit: str
|
|
89
|
+
direction: Literal["maximize", "minimize"]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Comparison(_Payload):
|
|
93
|
+
baseline: str = NonEmpty
|
|
94
|
+
rule: str = NonEmpty
|
|
95
|
+
threshold: float
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class DataSet(_Payload):
|
|
99
|
+
source: str = NonEmpty
|
|
100
|
+
licence: str | None = None
|
|
101
|
+
role: DataRole
|
|
102
|
+
digest: str = NonEmpty
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Budget(_Payload):
|
|
106
|
+
wall_clock_s: float = Field(gt=0)
|
|
107
|
+
compute: Any = None
|
|
108
|
+
money_usd_estimate: float = Field(ge=0)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class Seeds(_Payload):
|
|
112
|
+
policy: str = NonEmpty
|
|
113
|
+
values: list[int] = Field(min_length=1)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class PreregRegistered(_Payload):
|
|
117
|
+
hypothesis: str = NonEmpty
|
|
118
|
+
stage: str = NonEmpty
|
|
119
|
+
metric: Metric
|
|
120
|
+
comparison: Comparison
|
|
121
|
+
evaluation_data: list[DataSet] = Field(min_length=1)
|
|
122
|
+
budget: Budget
|
|
123
|
+
seeds: Seeds
|
|
124
|
+
code_digest: str = NonEmpty
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class PreregAmended(_Payload):
|
|
128
|
+
supersedes: str = NonEmpty
|
|
129
|
+
changes: dict[str, Any] = Field(min_length=1)
|
|
130
|
+
reason: str = NonEmpty
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class ExperimentStarted(_Payload):
|
|
134
|
+
prereg: str = NonEmpty
|
|
135
|
+
executor_id: str = NonEmpty
|
|
136
|
+
isolation: Literal["container", "vm", "none"]
|
|
137
|
+
code_digest: str = NonEmpty
|
|
138
|
+
env_digest: str = NonEmpty
|
|
139
|
+
seed: int
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class MetricValue(_Payload):
|
|
143
|
+
name: str = NonEmpty
|
|
144
|
+
value: float
|
|
145
|
+
unit: str
|
|
146
|
+
data_role: DataRole
|
|
147
|
+
set: str | None = None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class ExperimentFinished(_Payload):
|
|
151
|
+
experiment: str = NonEmpty
|
|
152
|
+
status: Literal["ok", "failed", "crashed", "timeout", "killed", "lost"]
|
|
153
|
+
metrics: list[MetricValue] = []
|
|
154
|
+
artifacts: dict[str, str] = {}
|
|
155
|
+
duration_s: float | None = None
|
|
156
|
+
cost_usd: float | None = None
|
|
157
|
+
metered: bool = False
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class MetricUse(_Payload):
|
|
161
|
+
experiment: str = NonEmpty # an experiment.finished entry id
|
|
162
|
+
name: str = NonEmpty
|
|
163
|
+
set: str | None = None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class DecisionMade(_Payload):
|
|
167
|
+
hypothesis: str = NonEmpty
|
|
168
|
+
stage: str = NonEmpty
|
|
169
|
+
outcome: Literal["promote", "discard", "confirm", "inconclusive"]
|
|
170
|
+
reason_code: ReasonCode | None = None
|
|
171
|
+
reason_text: str = NonEmpty
|
|
172
|
+
prereg: str | None = None
|
|
173
|
+
experiments: list[str] = [] # experiment.started entry ids
|
|
174
|
+
metrics_used: list[MetricUse] = []
|
|
175
|
+
comparison: dict[str, Any] = {}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class SelfcheckResult(_Payload):
|
|
179
|
+
paper: str = NonEmpty
|
|
180
|
+
assessment: dict[str, Any]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class PaperBuilt(_Payload):
|
|
184
|
+
artifacts: dict[str, str]
|
|
185
|
+
trace_map: dict[str, Any]
|
|
186
|
+
citation_keys: list[str]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class Correction(_Payload):
|
|
190
|
+
corrects: str = NonEmpty
|
|
191
|
+
problem: str = NonEmpty
|
|
192
|
+
correct: Any
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
SCHEMAS: dict[str, type[_Payload]] = {
|
|
196
|
+
"run.created": RunCreated,
|
|
197
|
+
"run.status": RunStatus,
|
|
198
|
+
"checkpoint.requested": CheckpointRequested,
|
|
199
|
+
"checkpoint.decided": CheckpointDecided,
|
|
200
|
+
"call.recorded": CallRecorded,
|
|
201
|
+
"idea.proposed": IdeaProposed,
|
|
202
|
+
"hypothesis.proposed": HypothesisProposed,
|
|
203
|
+
"hypothesis.novelty": HypothesisNovelty,
|
|
204
|
+
"screening.result": ScreeningResult,
|
|
205
|
+
"prereg.registered": PreregRegistered,
|
|
206
|
+
"prereg.amended": PreregAmended,
|
|
207
|
+
"experiment.started": ExperimentStarted,
|
|
208
|
+
"experiment.finished": ExperimentFinished,
|
|
209
|
+
"decision.made": DecisionMade,
|
|
210
|
+
"selfcheck.result": SelfcheckResult,
|
|
211
|
+
"paper.built": PaperBuilt,
|
|
212
|
+
"correction": Correction,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def validate_payload(entry_type: str, payload: dict[str, Any]) -> _Payload:
|
|
217
|
+
schema = SCHEMAS.get(entry_type)
|
|
218
|
+
if schema is None:
|
|
219
|
+
raise LedgerRejection("schema", f"unknown entry type {entry_type!r}")
|
|
220
|
+
try:
|
|
221
|
+
return schema.model_validate(payload)
|
|
222
|
+
except ValidationError as exc:
|
|
223
|
+
# Field locations and error kinds only: never echo input values (INV-7).
|
|
224
|
+
problems = "; ".join(
|
|
225
|
+
f"{'.'.join(str(p) for p in err['loc']) or '$'}: {err['type']}"
|
|
226
|
+
for err in exc.errors(include_input=False, include_url=False)
|
|
227
|
+
)
|
|
228
|
+
raise LedgerRejection("schema", f"{entry_type}: {problems}") from None
|
ledger/rules.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Replay index and write-time rules (04-ledger.md §5, §7 INV-3 to INV-6).
|
|
2
|
+
|
|
3
|
+
`RunIndex` is built by replaying entries in order. The same replay serves `state()` and the
|
|
4
|
+
rules, so what the rules check is exactly what a resumed run sees.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from . import payloads as p
|
|
13
|
+
from .errors import LedgerRejection
|
|
14
|
+
from .model import CONFIRM_STAGE, SMOKE_STAGE, Entry, RunState
|
|
15
|
+
|
|
16
|
+
# Outcomes that need experiments, and reason codes that can stand without them
|
|
17
|
+
# (for example a novelty discard before anything ran).
|
|
18
|
+
_NEEDS_EXPERIMENTS = {"promote", "confirm"}
|
|
19
|
+
_REASONS_WITHOUT_EXPERIMENTS = {"not_novel", "infeasible", "harness_error", "superseded",
|
|
20
|
+
"user_stopped", "limit_reached"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class RunIndex:
|
|
25
|
+
run_id: str
|
|
26
|
+
by_id: dict[str, Entry] = field(default_factory=dict)
|
|
27
|
+
status: str | None = None
|
|
28
|
+
hypotheses: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
29
|
+
prereg_key: dict[str, tuple[str, str]] = field(default_factory=dict)
|
|
30
|
+
in_force: dict[tuple[str, str], str] = field(default_factory=dict)
|
|
31
|
+
started_prereg: dict[str, str] = field(default_factory=dict)
|
|
32
|
+
finished_for: dict[str, str] = field(default_factory=dict)
|
|
33
|
+
finished_of: dict[str, str] = field(default_factory=dict)
|
|
34
|
+
decisions: list[str] = field(default_factory=list)
|
|
35
|
+
last_seq: int = 0
|
|
36
|
+
last_hash: str | None = None
|
|
37
|
+
|
|
38
|
+
def apply(self, e: Entry) -> None:
|
|
39
|
+
self.by_id[e.entry_id] = e
|
|
40
|
+
self.last_seq = e.seq
|
|
41
|
+
self.last_hash = e.hash
|
|
42
|
+
pl = e.payload
|
|
43
|
+
if e.type == "run.status":
|
|
44
|
+
self.status = pl["status"]
|
|
45
|
+
elif e.type == "hypothesis.proposed":
|
|
46
|
+
self.hypotheses[e.entry_id] = {"statement": pl["statement"], "kind": pl["kind"],
|
|
47
|
+
"idea": pl["idea"]}
|
|
48
|
+
elif e.type == "prereg.registered":
|
|
49
|
+
key = (pl["hypothesis"], pl["stage"])
|
|
50
|
+
self.prereg_key[e.entry_id] = key
|
|
51
|
+
self.in_force[key] = e.entry_id
|
|
52
|
+
elif e.type == "prereg.amended":
|
|
53
|
+
key = self.prereg_key[pl["supersedes"]]
|
|
54
|
+
self.prereg_key[e.entry_id] = key
|
|
55
|
+
self.in_force[key] = e.entry_id
|
|
56
|
+
elif e.type == "experiment.started":
|
|
57
|
+
self.started_prereg[e.entry_id] = pl["prereg"]
|
|
58
|
+
elif e.type == "experiment.finished":
|
|
59
|
+
self.finished_for[pl["experiment"]] = e.entry_id
|
|
60
|
+
self.finished_of[e.entry_id] = pl["experiment"]
|
|
61
|
+
elif e.type == "decision.made":
|
|
62
|
+
self.decisions.append(e.entry_id)
|
|
63
|
+
|
|
64
|
+
def open_experiments(self) -> list[str]:
|
|
65
|
+
return [s for s in self.started_prereg if s not in self.finished_for]
|
|
66
|
+
|
|
67
|
+
def to_state(self) -> RunState:
|
|
68
|
+
return RunState(
|
|
69
|
+
run_id=self.run_id, status=self.status, hypotheses=dict(self.hypotheses),
|
|
70
|
+
prereg_in_force=dict(self.in_force), open_experiments=self.open_experiments(),
|
|
71
|
+
finished=dict(self.finished_for), decisions=list(self.decisions),
|
|
72
|
+
last_seq=self.last_seq,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _require_ref(refs: list[str], entry_id: str, field_name: str) -> None:
|
|
77
|
+
if entry_id not in refs:
|
|
78
|
+
raise LedgerRejection("refs", f"{field_name} {entry_id!r} must also be listed in refs")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _require_type(idx: RunIndex, entry_id: str, types: tuple[str, ...], what: str) -> Entry:
|
|
82
|
+
e = idx.by_id.get(entry_id)
|
|
83
|
+
if e is None or e.type not in types:
|
|
84
|
+
raise LedgerRejection("refs", f"{what} {entry_id!r} is not a {' or '.join(types)} "
|
|
85
|
+
"entry of this run")
|
|
86
|
+
return e
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def check(idx: RunIndex, entry_type: str, model: p._Payload, refs: list[str]) -> None:
|
|
90
|
+
"""Raise LedgerRejection if the new entry breaks a rule. Order: refs, then type rules."""
|
|
91
|
+
for ref in refs:
|
|
92
|
+
if ref not in idx.by_id:
|
|
93
|
+
raise LedgerRejection("refs", f"ref {ref!r} does not exist earlier in this run")
|
|
94
|
+
|
|
95
|
+
if entry_type == "run.created":
|
|
96
|
+
if idx.last_seq != 0:
|
|
97
|
+
raise LedgerRejection("run.created", "a run has exactly one run.created, first")
|
|
98
|
+
return
|
|
99
|
+
if idx.last_seq == 0:
|
|
100
|
+
raise LedgerRejection("run.created", "the first entry of a run must be run.created")
|
|
101
|
+
|
|
102
|
+
if isinstance(model, p.HypothesisProposed):
|
|
103
|
+
_require_ref(refs, model.idea, "idea")
|
|
104
|
+
_require_type(idx, model.idea, ("idea.proposed",), "idea")
|
|
105
|
+
elif isinstance(model, p.HypothesisNovelty):
|
|
106
|
+
_require_ref(refs, model.hypothesis, "hypothesis")
|
|
107
|
+
_require_type(idx, model.hypothesis, ("hypothesis.proposed",), "hypothesis")
|
|
108
|
+
elif isinstance(model, p.PreregRegistered):
|
|
109
|
+
_check_prereg(idx, model, refs)
|
|
110
|
+
elif isinstance(model, p.PreregAmended):
|
|
111
|
+
_require_ref(refs, model.supersedes, "supersedes")
|
|
112
|
+
_require_type(idx, model.supersedes, ("prereg.registered", "prereg.amended"),
|
|
113
|
+
"supersedes")
|
|
114
|
+
elif isinstance(model, p.ExperimentStarted):
|
|
115
|
+
_check_started(idx, model, refs) # INV-3
|
|
116
|
+
elif isinstance(model, p.ExperimentFinished):
|
|
117
|
+
_check_finished(idx, model, refs) # INV-6
|
|
118
|
+
elif isinstance(model, p.DecisionMade):
|
|
119
|
+
_check_decision(idx, model, refs) # INV-4, INV-5
|
|
120
|
+
elif isinstance(model, p.Correction):
|
|
121
|
+
_require_ref(refs, model.corrects, "corrects")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _check_prereg(idx: RunIndex, m: p.PreregRegistered, refs: list[str]) -> None:
|
|
125
|
+
_require_ref(refs, m.hypothesis, "hypothesis")
|
|
126
|
+
_require_type(idx, m.hypothesis, ("hypothesis.proposed",), "hypothesis")
|
|
127
|
+
_require_ref(refs, m.comparison.baseline, "comparison.baseline")
|
|
128
|
+
_require_type(idx, m.comparison.baseline, ("hypothesis.proposed",), "comparison.baseline")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _check_started(idx: RunIndex, m: p.ExperimentStarted, refs: list[str]) -> None:
|
|
132
|
+
_require_ref(refs, m.prereg, "prereg")
|
|
133
|
+
_require_type(idx, m.prereg, ("prereg.registered", "prereg.amended"), "prereg")
|
|
134
|
+
key = idx.prereg_key[m.prereg]
|
|
135
|
+
if idx.in_force.get(key) != m.prereg:
|
|
136
|
+
raise LedgerRejection("INV-3", f"prereg {m.prereg!r} is not the pre-registration in "
|
|
137
|
+
f"force for hypothesis {key[0]!r} at stage {key[1]!r}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _check_finished(idx: RunIndex, m: p.ExperimentFinished, refs: list[str]) -> None:
|
|
141
|
+
_require_ref(refs, m.experiment, "experiment")
|
|
142
|
+
_require_type(idx, m.experiment, ("experiment.started",), "experiment")
|
|
143
|
+
if m.experiment in idx.finished_for:
|
|
144
|
+
raise LedgerRejection("INV-6", f"experiment {m.experiment!r} already has an "
|
|
145
|
+
"experiment.finished entry")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _check_decision(idx: RunIndex, m: p.DecisionMade, refs: list[str]) -> None:
|
|
149
|
+
if m.stage == SMOKE_STAGE:
|
|
150
|
+
raise LedgerRejection("INV-9", "no decision is made at stage `smoke` (harness checks)")
|
|
151
|
+
_require_ref(refs, m.hypothesis, "hypothesis")
|
|
152
|
+
_require_type(idx, m.hypothesis, ("hypothesis.proposed",), "hypothesis")
|
|
153
|
+
|
|
154
|
+
if m.outcome in {"discard", "inconclusive"} and m.reason_code is None:
|
|
155
|
+
raise LedgerRejection("reasons", f"a {m.outcome} decision needs a reason_code")
|
|
156
|
+
|
|
157
|
+
if not m.experiments:
|
|
158
|
+
if m.outcome in _NEEDS_EXPERIMENTS or m.reason_code not in _REASONS_WITHOUT_EXPERIMENTS:
|
|
159
|
+
raise LedgerRejection("INV-4", f"a {m.outcome} decision with reason "
|
|
160
|
+
f"{m.reason_code!r} must cite the experiments it used")
|
|
161
|
+
if m.metrics_used:
|
|
162
|
+
raise LedgerRejection("INV-4", "metrics_used without experiments")
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
# INV-4: cite the pre-registration in force at each start, and each finished entry.
|
|
166
|
+
if m.prereg is None:
|
|
167
|
+
raise LedgerRejection("INV-4", "a decision that uses experiments must name its prereg")
|
|
168
|
+
_require_ref(refs, m.prereg, "prereg")
|
|
169
|
+
key = idx.prereg_key.get(m.prereg)
|
|
170
|
+
if key is None:
|
|
171
|
+
raise LedgerRejection("INV-4", f"prereg {m.prereg!r} is not a pre-registration")
|
|
172
|
+
if key != (m.hypothesis, m.stage):
|
|
173
|
+
raise LedgerRejection("INV-4", "the prereg does not belong to this hypothesis and stage")
|
|
174
|
+
finished_ids: set[str] = set()
|
|
175
|
+
for started in m.experiments:
|
|
176
|
+
_require_type(idx, started, ("experiment.started",), "experiment")
|
|
177
|
+
if idx.started_prereg[started] != m.prereg:
|
|
178
|
+
raise LedgerRejection("INV-4", f"experiment {started!r} started under a different "
|
|
179
|
+
"pre-registration")
|
|
180
|
+
finished = idx.finished_for.get(started)
|
|
181
|
+
if finished is None:
|
|
182
|
+
raise LedgerRejection("INV-4", f"experiment {started!r} has not finished")
|
|
183
|
+
_require_ref(refs, finished, "finished experiment")
|
|
184
|
+
finished_ids.add(finished)
|
|
185
|
+
|
|
186
|
+
# A decision computed from metrics must say which, or INV-5 could not be checked.
|
|
187
|
+
if not m.metrics_used and (m.outcome in _NEEDS_EXPERIMENTS or m.reason_code == "refuted"):
|
|
188
|
+
raise LedgerRejection("INV-5", f"a {m.outcome} decision must list the metrics it used")
|
|
189
|
+
|
|
190
|
+
# INV-5 (KB-014): held-out metrics only in decisions at the confirmation stage.
|
|
191
|
+
for use in m.metrics_used:
|
|
192
|
+
if use.experiment not in finished_ids:
|
|
193
|
+
raise LedgerRejection("INV-4", f"metric source {use.experiment!r} is not a finished "
|
|
194
|
+
"experiment of this decision")
|
|
195
|
+
metrics = idx.by_id[use.experiment].payload.get("metrics", [])
|
|
196
|
+
match = [x for x in metrics if x["name"] == use.name
|
|
197
|
+
and (use.set is None or x.get("set") == use.set)]
|
|
198
|
+
if not match:
|
|
199
|
+
raise LedgerRejection("INV-4", f"metric {use.name!r} is not recorded in "
|
|
200
|
+
f"{use.experiment!r}")
|
|
201
|
+
held_out = any(x["data_role"] == "held_out" for x in match)
|
|
202
|
+
if held_out and m.stage != CONFIRM_STAGE:
|
|
203
|
+
raise LedgerRejection("INV-5", f"a {m.outcome} decision at stage {m.stage!r} may "
|
|
204
|
+
"not use held-out metrics")
|