quantlab-factor-agent 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.
- factor_agent/KUNQUANT_LICENSE +201 -0
- factor_agent/__init__.py +8 -0
- factor_agent/_acceptance_model.py +93 -0
- factor_agent/_admission.py +168 -0
- factor_agent/_expression_fold.py +48 -0
- factor_agent/_feedback.py +399 -0
- factor_agent/_max_drawdown.py +51 -0
- factor_agent/_model_cache.py +100 -0
- factor_agent/_scheduler.py +35 -0
- factor_agent/acceptance.py +808 -0
- factor_agent/accepted/__init__.py +1 -0
- factor_agent/aggregations.py +122 -0
- factor_agent/campaign.py +797 -0
- factor_agent/candidate.py +51 -0
- factor_agent/config.py +651 -0
- factor_agent/derived.py +98 -0
- factor_agent/evaluator.py +1096 -0
- factor_agent/events.py +343 -0
- factor_agent/exported.py +177 -0
- factor_agent/gate/__init__.py +359 -0
- factor_agent/gate/_canonical.py +331 -0
- factor_agent/gate/_checks.py +85 -0
- factor_agent/gate/_constraints.py +85 -0
- factor_agent/gate/_dimension.py +177 -0
- factor_agent/gate/_originality.py +146 -0
- factor_agent/gate/_sandbox.py +105 -0
- factor_agent/gate/graph.py +223 -0
- factor_agent/gate/stages.py +383 -0
- factor_agent/gate/vocabulary.py +1097 -0
- factor_agent/ledger.py +85 -0
- factor_agent/llm/__init__.py +0 -0
- factor_agent/llm/_common.py +18 -0
- factor_agent/llm/_raw.py +78 -0
- factor_agent/llm/anthropic.py +100 -0
- factor_agent/llm/base.py +124 -0
- factor_agent/llm/claude_code.py +101 -0
- factor_agent/llm/log.py +74 -0
- factor_agent/llm/openai_compatible.py +113 -0
- factor_agent/llm/replay.py +59 -0
- factor_agent/llm/scripted.py +39 -0
- factor_agent/period_lag.py +91 -0
- factor_agent/prompts.py +557 -0
- factor_agent/researcher.py +615 -0
- factor_agent/scoring.py +143 -0
- factor_agent/segments.py +184 -0
- factor_agent/session.py +500 -0
- factor_agent/snapshot.py +219 -0
- factor_agent/web/__init__.py +7 -0
- factor_agent/web/_acceptance.py +145 -0
- factor_agent/web/_config.py +51 -0
- factor_agent/web/_diagnosis.py +110 -0
- factor_agent/web/_files.py +78 -0
- factor_agent/web/_health.py +207 -0
- factor_agent/web/_ic.py +27 -0
- factor_agent/web/_ledger.py +293 -0
- factor_agent/web/_memory.py +42 -0
- factor_agent/web/_results.py +77 -0
- factor_agent/web/_round.py +219 -0
- factor_agent/web/_trials.py +112 -0
- factor_agent/web/app.py +118 -0
- factor_agent/web/run_view.py +329 -0
- factor_agent/web/static/assets/index-9QjjJgl3.css +1 -0
- factor_agent/web/static/assets/index-iOE9ei_S.js +74 -0
- factor_agent/web/static/index.html +13 -0
- factor_agent/zoo.py +136 -0
- quantlab_factor_agent-0.1.0.dist-info/METADATA +172 -0
- quantlab_factor_agent-0.1.0.dist-info/RECORD +72 -0
- quantlab_factor_agent-0.1.0.dist-info/WHEEL +5 -0
- quantlab_factor_agent-0.1.0.dist-info/entry_points.txt +2 -0
- quantlab_factor_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
- quantlab_factor_agent-0.1.0.dist-info/licenses/factor_agent/KUNQUANT_LICENSE +201 -0
- quantlab_factor_agent-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""Read a run directory as the frontend shows it, without writing to it.
|
|
2
|
+
|
|
3
|
+
``list_runs(root)`` finds every run directory under a folder: a directory
|
|
4
|
+
holding a ``snapshot.json``, which a campaign writes on its first start,
|
|
5
|
+
before its first round. ``RunView.read(run_dir)`` reads one run. Callers
|
|
6
|
+
never see the run directory's files; this module and its private modules
|
|
7
|
+
alone know them, and read them while a campaign may be writing them:
|
|
8
|
+
|
|
9
|
+
- The state file decides how many rounds are written. The campaign appends
|
|
10
|
+
a round's trials to the ledger before it rewrites the state, so ledger
|
|
11
|
+
rounds beyond the state's count are not read until the state catches up,
|
|
12
|
+
and a ledger line a crash cut short is skipped.
|
|
13
|
+
- NaN and infinite values in the ledger (a correlation with an empty pool,
|
|
14
|
+
say) read as ``None``, so a view converts to strict JSON.
|
|
15
|
+
- The run list reads of each run only what it shows; a run whose files
|
|
16
|
+
cannot be read is listed as unreadable, with the reason.
|
|
17
|
+
- The events file (``factor_agent.events``) tells what the run is doing:
|
|
18
|
+
its phase, its calls and their tokens, its process, and the settings it
|
|
19
|
+
was opened with. A run a campaign without events wrote has none of this
|
|
20
|
+
but the calls of its round in progress, counted from its call files.
|
|
21
|
+
- The round in progress is replayed from the events since the run was
|
|
22
|
+
last opened: its calls, and a pending trial per slot of its plan. A
|
|
23
|
+
round a resume dropped left its events before the resume, so it is not
|
|
24
|
+
among them. A run without events has no round in progress to show.
|
|
25
|
+
- An LLM call is read by the number of its call file, from the file
|
|
26
|
+
itself: tokens, truncation and the model's reasoning are the provider's
|
|
27
|
+
reply's; what it concerned (round, slots) only the events file says.
|
|
28
|
+
- A resumed run keeps the stop of its previous open in its state file until
|
|
29
|
+
it stops again. With events, a stop counts only when a stop event follows
|
|
30
|
+
the latest start or resume. Without, it counts only when the state file
|
|
31
|
+
was written after the run's latest open and no LLM call ended after it: a
|
|
32
|
+
resumed run that has asked the LLM since, or has written no state since
|
|
33
|
+
it was opened, has not stopped.
|
|
34
|
+
- A round's t threshold (one per round, since a round's candidates are
|
|
35
|
+
judged at the same trial count) is the one its admission events give.
|
|
36
|
+
Without them it is the one its refusals quote, carried over from the
|
|
37
|
+
round before when the round quotes none; an accepted factor says which.
|
|
38
|
+
The correlation limit is the one the run was last opened with, or without
|
|
39
|
+
events the one a refusal quotes. Either is ``None`` while nothing gives it.
|
|
40
|
+
- The process that opened the run last is looked for only on the host it
|
|
41
|
+
ran on; on another host it is unknown, never gone.
|
|
42
|
+
- An Acceptance report is ``acceptance/<segment>[_<digest>].json``; one that
|
|
43
|
+
records opens of the hold-out is a hold-out report. The opens are the hold-out
|
|
44
|
+
lock's. A report written before the contribution table or the selection
|
|
45
|
+
flag existed reads with ``None`` for them: nothing it lacks is made up.
|
|
46
|
+
- A collision is read from a refusal's text: the Gate's rewrite, refinement
|
|
47
|
+
of parameters only or shared subtree is a code collision with each factor
|
|
48
|
+
it names, admission's correlation above the limit a ranking collision. The
|
|
49
|
+
Gate's refusal of a duplicate of the run's own earlier candidate is not.
|
|
50
|
+
- The diagnosis is read from the trials by rules private to this package,
|
|
51
|
+
their thresholds fixed there, each finding counting trials (not attempts:
|
|
52
|
+
a trial is counted once whichever of its versions met the problem).
|
|
53
|
+
- A trial the Evaluator scored is placed on the admission map by its
|
|
54
|
+
partial-IC t in its direction (the sign of its training IC, as admission
|
|
55
|
+
reads it) and its largest absolute correlation with an existing factor.
|
|
56
|
+
The trials it never scored are grouped by the stage that refused them and
|
|
57
|
+
the reason: reasons that differ only in their numbers, or in the reply or
|
|
58
|
+
code they quote, are one reason.
|
|
59
|
+
- A trial, and an accepted factor, is intraday when the graph the Gate
|
|
60
|
+
built for it holds a session aggregation: an operator of the run's
|
|
61
|
+
snapshotted operator file whose class is a ``SessionAggregation``. A run
|
|
62
|
+
whose snapshot records no intraday dataset has none.
|
|
63
|
+
- The memory's versions are the distiller's files ``memory/round_NN.md``,
|
|
64
|
+
each written before round ``NN``, so after round ``NN - 1``. A version
|
|
65
|
+
after the first is compared with the one before it line by line.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
from collections.abc import Iterator
|
|
69
|
+
from contextlib import contextmanager
|
|
70
|
+
from dataclasses import dataclass
|
|
71
|
+
from datetime import datetime, timezone
|
|
72
|
+
from pathlib import Path
|
|
73
|
+
|
|
74
|
+
from factor_agent import events
|
|
75
|
+
from factor_agent.web._config import ConfigCopy, RunFiles, SnapshotFile, read_files
|
|
76
|
+
from factor_agent.web._ic import TrialIC, read_ic
|
|
77
|
+
from factor_agent.web._acceptance import AcceptanceReports, Comparison, Contribution, HoldoutOpen, acceptance
|
|
78
|
+
from factor_agent.web._diagnosis import Finding, diagnosis
|
|
79
|
+
from factor_agent.web._files import LineChange, Replayed, read_json, utc, written
|
|
80
|
+
from factor_agent.web._health import (
|
|
81
|
+
RECENT_ROUNDS, STALL_ASKING, Health, RoundTokens, Stop, Tokens, health, process_state, stop,
|
|
82
|
+
)
|
|
83
|
+
from factor_agent.web._ledger import (
|
|
84
|
+
Collision, Neighbour, Trial, aggregations, correlation_limit, read_trials, statuses,
|
|
85
|
+
)
|
|
86
|
+
from factor_agent.web._memory import MemoryLine, MemoryVersion, memory
|
|
87
|
+
from factor_agent.web._results import Factor, factors
|
|
88
|
+
from factor_agent.web._round import Check, LLMCall, PendingTrial, RoundCall, RunningRound, read_call, running
|
|
89
|
+
from factor_agent.web._trials import Placed, Refusal, TrialRef, trials_page
|
|
90
|
+
|
|
91
|
+
__all__ = [
|
|
92
|
+
"RECENT_ROUNDS", "STALL_ASKING", "AcceptanceReports", "AdmissionLimits", "Check", "Collision", "Comparison",
|
|
93
|
+
"ConfigCopy", "Contribution", "Factor", "Finding", "Health", "HoldoutOpen", "LLMCall", "LineChange", "MemoryLine",
|
|
94
|
+
"MemoryVersion", "Neighbour",
|
|
95
|
+
"PendingTrial", "Placed", "Refusal", "RoundCall", "RoundTokens", "RunFiles", "RunSummary", "RunView", "RunningRound", "SnapshotFile", "Stop",
|
|
96
|
+
"Tokens", "Trial", "TrialIC", "TrialRef", "find_run", "list_runs", "read_call", "read_files", "read_ic", "run_version",
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class AdmissionLimits:
|
|
102
|
+
"""The least coverage and decile monotonicity admission required, as the run's last start recorded them."""
|
|
103
|
+
|
|
104
|
+
min_coverage: float
|
|
105
|
+
min_monotonicity: float
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True)
|
|
109
|
+
class RunSummary:
|
|
110
|
+
"""What the run list shows of a run.
|
|
111
|
+
|
|
112
|
+
``name`` is the run directory's path relative to the listed folder
|
|
113
|
+
(the folder's own name when the folder is the run directory).
|
|
114
|
+
``code_version`` is the commit of this package the run was last opened
|
|
115
|
+
with, ``"unknown"`` when it could not be read. ``rounds`` counts the
|
|
116
|
+
written rounds, ``trials`` and ``accepted`` their trials and accepted
|
|
117
|
+
ones. ``stop`` is ``None`` while the run has not stopped, or crashed
|
|
118
|
+
without stopping. ``updated`` is the time of the run's last write (UTC).
|
|
119
|
+
``unreadable`` says which file of the run could not be read and why,
|
|
120
|
+
for a run the list could not read (its counts are then 0); ``None``
|
|
121
|
+
for any other. ``state`` is ``"stopped"``, ``"running"`` while the
|
|
122
|
+
process that opened it last still runs, ``"crashed"`` once that process
|
|
123
|
+
exited without recording a stop, or ``"unknown"`` when the process cannot
|
|
124
|
+
be checked: a run without an events file, or opened on another host.
|
|
125
|
+
``max_rounds`` is its round limit, ``None`` without an events file.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
name: str
|
|
129
|
+
code_version: str
|
|
130
|
+
rounds: int
|
|
131
|
+
trials: int
|
|
132
|
+
accepted: int
|
|
133
|
+
stop: Stop | None
|
|
134
|
+
updated: datetime
|
|
135
|
+
unreadable: str | None = None
|
|
136
|
+
state: str = "unknown"
|
|
137
|
+
max_rounds: int | None = None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass(frozen=True)
|
|
141
|
+
class RunView:
|
|
142
|
+
"""A read-only snapshot of one run: its summary, the trials of its written rounds, its accepted factors and its memory.
|
|
143
|
+
|
|
144
|
+
``factors`` holds the accepted factors in the order they were accepted
|
|
145
|
+
(by round, then slot). ``correlation_limit`` is the largest absolute
|
|
146
|
+
correlation with an existing factor that admission allows: the one the
|
|
147
|
+
run was last opened with, or for a run without events the one its
|
|
148
|
+
latest refusal for it quotes, ``None`` while nothing gives it.
|
|
149
|
+
``admission`` holds the least coverage and monotonicity admission
|
|
150
|
+
required, from the run's last start; ``None`` for a run without events.
|
|
151
|
+
``evaluated`` places every trial the Evaluator scored, in round and slot
|
|
152
|
+
order, and ``threshold`` is the t threshold of the latest round (see the
|
|
153
|
+
module's notes), ``None`` while nothing gives it.
|
|
154
|
+
``never_evaluated`` groups the trials it never scored, by stage (parse,
|
|
155
|
+
Gate, review, then evaluation) and then by the first round of each reason.
|
|
156
|
+
``memory`` holds the versions of the memory by the round after which
|
|
157
|
+
they were written, the earliest first; it is empty while the distiller
|
|
158
|
+
has written none. ``acceptance`` holds the run's Acceptance reports and
|
|
159
|
+
hold-out opens. ``diagnosis`` holds at most three findings, the one
|
|
160
|
+
concerning the most trials first; it is empty when no rule holds.
|
|
161
|
+
``health`` is what the run is doing now, and ``running`` the round in
|
|
162
|
+
progress: ``None`` when the run has stopped, or has no events file (its
|
|
163
|
+
health then counts the round's calls by role). ``now`` is the time the
|
|
164
|
+
view describes (UTC).
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
summary: RunSummary
|
|
168
|
+
trials: tuple[Trial, ...]
|
|
169
|
+
factors: tuple[Factor, ...]
|
|
170
|
+
correlation_limit: float | None
|
|
171
|
+
evaluated: tuple[Placed, ...]
|
|
172
|
+
threshold: float | None
|
|
173
|
+
never_evaluated: tuple[Refusal, ...]
|
|
174
|
+
memory: tuple[MemoryVersion, ...]
|
|
175
|
+
acceptance: AcceptanceReports
|
|
176
|
+
diagnosis: tuple[Finding, ...]
|
|
177
|
+
admission: AdmissionLimits | None
|
|
178
|
+
health: Health
|
|
179
|
+
running: RunningRound | None
|
|
180
|
+
now: datetime
|
|
181
|
+
|
|
182
|
+
@classmethod
|
|
183
|
+
def read(cls, run_dir: str | Path, now: datetime | None = None, name: str | None = None) -> "RunView":
|
|
184
|
+
"""Read the run in ``run_dir``.
|
|
185
|
+
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
run_dir : str or Path
|
|
189
|
+
A run directory: one holding a ``snapshot.json``.
|
|
190
|
+
now : datetime, optional
|
|
191
|
+
The time the view describes; the current time by default.
|
|
192
|
+
name : str, optional
|
|
193
|
+
The run's name in the view; the directory's name by default.
|
|
194
|
+
|
|
195
|
+
Returns
|
|
196
|
+
-------
|
|
197
|
+
RunView
|
|
198
|
+
|
|
199
|
+
Raises
|
|
200
|
+
------
|
|
201
|
+
FileNotFoundError
|
|
202
|
+
When ``run_dir`` holds no ``snapshot.json``.
|
|
203
|
+
"""
|
|
204
|
+
run_dir = Path(run_dir)
|
|
205
|
+
if not (run_dir / "snapshot.json").is_file():
|
|
206
|
+
raise FileNotFoundError(f"web: {run_dir} is not a run directory: it holds no snapshot.json")
|
|
207
|
+
now = now or datetime.now(timezone.utc)
|
|
208
|
+
state = read_json(run_dir / "state.json") or {}
|
|
209
|
+
trials = read_trials(run_dir / "ledger.jsonl", state.get("rounds", 0), aggregations(run_dir))
|
|
210
|
+
replayed = Replayed.read(run_dir)
|
|
211
|
+
summary = _summary(run_dir, name or run_dir.name, state, replayed, [t.status for t in trials])
|
|
212
|
+
page = trials_page(trials, replayed)
|
|
213
|
+
return cls(
|
|
214
|
+
summary=summary, trials=trials, factors=factors(trials, replayed),
|
|
215
|
+
correlation_limit=correlation_limit(trials, replayed), evaluated=page.evaluated, threshold=page.threshold,
|
|
216
|
+
never_evaluated=page.never_evaluated, memory=memory(run_dir / "memory"), acceptance=acceptance(run_dir),
|
|
217
|
+
diagnosis=diagnosis(trials),
|
|
218
|
+
admission=AdmissionLimits(replayed.opened.min_coverage, replayed.opened.min_monotonicity)
|
|
219
|
+
if replayed.opened else None,
|
|
220
|
+
health=health(run_dir, state, summary.rounds, summary.trials, summary.stop is not None, replayed, now),
|
|
221
|
+
running=None if summary.stop else running(summary.rounds + 1, replayed), now=now,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _summary(run_dir: Path, name: str, state: dict, replayed: Replayed, written_statuses: list[str]) -> RunSummary:
|
|
226
|
+
"""The summary of a run whose state file holds ``state`` and whose written trials have ``written_statuses``."""
|
|
227
|
+
versions = (read_json(run_dir / "snapshot.json") or {}).get("versions") or [{}]
|
|
228
|
+
stopped = stop(run_dir, state, versions[-1].get("opened_at"), replayed)
|
|
229
|
+
process = process_state(replayed.opened)
|
|
230
|
+
return RunSummary(
|
|
231
|
+
name=name, code_version=versions[-1].get("factor_agent") or "unknown", rounds=state.get("rounds", 0),
|
|
232
|
+
trials=len(written_statuses), accepted=written_statuses.count("accepted"),
|
|
233
|
+
stop=stopped, updated=utc(max(p.stat().st_mtime for p in written(run_dir))),
|
|
234
|
+
state="stopped" if stopped else {"running": "running", "gone": "crashed"}.get(process, "unknown"),
|
|
235
|
+
max_rounds=replayed.opened.max_rounds if replayed.opened else None,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class _Unreadable(Exception):
|
|
240
|
+
"""A run file the run list cannot read; the message names it and says why."""
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@contextmanager
|
|
244
|
+
def _reading(path: Path) -> Iterator[None]:
|
|
245
|
+
"""Turn a failure to read ``path`` (missing while listed, not JSON, of another shape) into ``_Unreadable``."""
|
|
246
|
+
try:
|
|
247
|
+
yield
|
|
248
|
+
except (OSError, ValueError, KeyError, TypeError, AttributeError) as exc:
|
|
249
|
+
raise _Unreadable(f"{path.name} cannot be read: {exc}") from exc
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _listed(run_dir: Path, name: str) -> RunSummary:
|
|
253
|
+
"""The run list's summary of a run: its state, ledger statuses and events only, or unreadable."""
|
|
254
|
+
try:
|
|
255
|
+
with _reading(run_dir / "state.json"):
|
|
256
|
+
state = read_json(run_dir / "state.json") or {}
|
|
257
|
+
rounds = state.get("rounds", 0)
|
|
258
|
+
with _reading(run_dir / "ledger.jsonl"):
|
|
259
|
+
found = statuses(run_dir / "ledger.jsonl", rounds)
|
|
260
|
+
with _reading(run_dir / events.FILE):
|
|
261
|
+
replayed = Replayed.read(run_dir)
|
|
262
|
+
with _reading(run_dir / "snapshot.json"):
|
|
263
|
+
return _summary(run_dir, name, state, replayed, found)
|
|
264
|
+
except _Unreadable as exc:
|
|
265
|
+
return RunSummary(name=name, code_version="unknown", rounds=0, trials=0, accepted=0, stop=None,
|
|
266
|
+
updated=utc((run_dir / "snapshot.json").stat().st_mtime), unreadable=str(exc))
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def run_version(run_dir: str | Path) -> str:
|
|
270
|
+
"""A text that changes whenever the run in ``run_dir`` writes, cheap to read: a page polls it before reading the run.
|
|
271
|
+
|
|
272
|
+
It is made of the modification times and sizes of the files a campaign
|
|
273
|
+
writes while it runs, and the number of its call files.
|
|
274
|
+
"""
|
|
275
|
+
run_dir = Path(run_dir)
|
|
276
|
+
parts = []
|
|
277
|
+
for name in ("snapshot.json", "state.json", "ledger.jsonl", events.FILE, "llm_log", "memory", "acceptance",
|
|
278
|
+
"holdout.lock"):
|
|
279
|
+
try:
|
|
280
|
+
stat = (run_dir / name).stat()
|
|
281
|
+
except FileNotFoundError:
|
|
282
|
+
parts.append("-")
|
|
283
|
+
continue
|
|
284
|
+
parts.append(f"{stat.st_mtime_ns}:{stat.st_size}")
|
|
285
|
+
return " ".join(parts)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def list_runs(root: str | Path) -> list[RunSummary]:
|
|
289
|
+
"""The runs under ``root``, the latest written first.
|
|
290
|
+
|
|
291
|
+
Each run is read only as far as its summary needs. A run whose files
|
|
292
|
+
cannot be read is listed with ``unreadable`` saying why, so one broken
|
|
293
|
+
run does not hide the others.
|
|
294
|
+
|
|
295
|
+
Parameters
|
|
296
|
+
----------
|
|
297
|
+
root : str or Path
|
|
298
|
+
A folder of run directories, at any depth, or a run directory itself.
|
|
299
|
+
|
|
300
|
+
Returns
|
|
301
|
+
-------
|
|
302
|
+
list of RunSummary
|
|
303
|
+
One per run directory, named by its path relative to ``root``.
|
|
304
|
+
"""
|
|
305
|
+
root = Path(root)
|
|
306
|
+
summaries = [_listed(d, name) for name, d in _run_dirs(root).items()]
|
|
307
|
+
return sorted(summaries, key=lambda s: s.updated, reverse=True)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def find_run(root: str | Path, name: str) -> Path | None:
|
|
311
|
+
"""The run directory ``list_runs(root)`` names ``name``, ``None`` when it names none so.
|
|
312
|
+
|
|
313
|
+
A name is the run's path under ``root``, so the run is looked up there without walking ``root``, which
|
|
314
|
+
may hold other work of many files; a name that leaves ``root`` names nothing.
|
|
315
|
+
"""
|
|
316
|
+
root = Path(root)
|
|
317
|
+
if name == root.name and (root / "snapshot.json").is_file():
|
|
318
|
+
return root
|
|
319
|
+
parts = Path(name).parts
|
|
320
|
+
if not parts or Path(name).is_absolute() or ".." in parts:
|
|
321
|
+
return None
|
|
322
|
+
found = root / name
|
|
323
|
+
return found if (found / "snapshot.json").is_file() else None
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _run_dirs(root: Path) -> dict[str, Path]:
|
|
327
|
+
"""The run directories under ``root`` by their names."""
|
|
328
|
+
found = sorted({p.parent for p in root.rglob("snapshot.json") if p.is_file()})
|
|
329
|
+
return {d.relative_to(root).as_posix() if d != root else root.name: d for d in found}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--bg: #fbfbfa;--fg: #1d1d1b;--mut: #7a7a74;--line: #e4e3de;--card: #fff;--good: #1f8a4c;--bad: #c2410c;--fail: #9ca3af;--acc: #2557d6;font:14px/1.45 ui-sans-serif,system-ui,sans-serif;color-scheme:light dark}@media(prefers-color-scheme:dark){:root{--bg: #151514;--fg: #e8e7e2;--mut: #8f8e88;--line: #2c2c2a;--card: #1d1d1c;--acc: #7aa2ff }}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg)}a{color:var(--acc);text-decoration:none}.muted{color:var(--mut)}.error{color:var(--bad)}.mono{font-family:ui-monospace,monospace;font-size:12px}.top{padding:10px 16px;border-bottom:1px solid var(--line);position:sticky;top:0;background:var(--bg)}.top a{font-weight:600;color:var(--fg)}main{padding:16px;overflow-x:auto}.facts{margin-top:0}.run-list section{margin:4px 0 28px}.run-list h2{font-size:17px;margin:0 0 12px;overflow-wrap:anywhere}.run-cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:12px}.run-card{display:grid;gap:6px;padding:14px;background:var(--card);color:var(--fg);text-decoration:none;border:1px solid var(--line);border-top:3px solid var(--line);border-radius:10px}a.run-card:hover{border-color:var(--acc)}.run-card.running{border-top-color:var(--good)}.run-card.crashed,.run-card.unreadable{border-top-color:var(--bad)}.run-card .head{display:flex;justify-content:space-between;align-items:center}.run-card .big{font-size:34px;font-weight:700;line-height:1}.run-card .big span{font-size:13px;font-weight:400;color:var(--mut)}.run-card .rate{height:5px;background:var(--line);border-radius:3px}.run-card .rate i{display:block;height:100%;background:var(--good);border-radius:3px}.pill{display:inline-flex;align-items:center;gap:5px;padding:1px 8px;border-radius:999px;font-size:12px;border:1px solid var(--line);color:var(--mut)}.pill.running{color:var(--good);border-color:color-mix(in srgb,var(--good) 50%,transparent)}.pill.crashed{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 50%,transparent)}.pill .dot{width:7px;height:7px;border-radius:50%;background:var(--good);animation:blink 1.6s infinite}@keyframes blink{50%{opacity:.3}}.badge{font-size:12px;padding:1px 7px;border-radius:9px;border:1px solid currentColor;white-space:nowrap}.badge.accepted{color:var(--good)}.badge.rejected{color:var(--bad)}.badge.failed{color:var(--fail)}.badge.intraday{color:var(--acc)}.pages{display:flex;gap:14px;margin-bottom:12px}.pages a.here{color:var(--fg);font-weight:600}.memory-version{border:1px solid var(--line);background:var(--card);border-radius:6px;padding:8px 12px;margin-bottom:10px}.memory-version>summary{cursor:pointer;font-weight:600}pre.memory{font-family:ui-monospace,monospace;font-size:12px;white-space:pre-wrap;margin:6px 0}.memory-added{background:color-mix(in srgb,var(--good) 16%,transparent)}.memory-removed{background:color-mix(in srgb,var(--bad) 14%,transparent);text-decoration:line-through}.small{font-size:12px}button.link{background:none;border:0;padding:0;font:inherit;color:var(--acc);cursor:pointer;text-align:left}.sorts{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px;margin:8px 0}.sorts h2{font-size:16px;margin:0 8px 0 0}.sorts button{font:inherit;font-size:12px;padding:2px 9px;border-radius:9px;border:1px solid var(--line);background:var(--card);color:var(--fg);cursor:pointer}.sorts button.on{border-color:var(--acc);color:var(--acc)}.factors{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,290px),1fr));gap:12px}.factor{border:1px solid var(--line);background:var(--card);border-radius:6px;padding:10px 12px;min-width:0}.factor.open{grid-column:1 / -1}.factor header{display:flex;justify-content:space-between;font-size:12px}.factor h3{font-size:14px;margin:4px 0 6px;overflow-wrap:anywhere}.factor .direction{font-weight:600;font-family:ui-monospace,monospace}.deciles{display:block;max-width:100%}.deciles .axis{stroke:var(--line)}.deciles .pos{fill:var(--good)}.deciles .neg{fill:var(--bad)}.margin{display:grid;grid-template-columns:auto 1fr;gap:2px 8px;align-items:center;margin:6px 0;font-size:12px}.margin small{grid-column:1 / -1;color:var(--mut)}.margin .bar{position:relative;height:6px;background:var(--line);border-radius:3px}.margin .bar i{position:absolute;left:0;top:0;bottom:0;background:var(--acc);border-radius:3px}.margin .bar em{position:absolute;top:-3px;bottom:-3px;width:2px;background:var(--fg)}dl.trust{display:grid;grid-template-columns:auto 1fr;gap:2px 10px;margin:6px 0 0;font-size:12px}dl.trust dt{color:var(--mut)}dl.trust dd{margin:0;overflow-wrap:anywhere}.write-up{border-top:1px solid var(--line);margin-top:10px;padding-top:4px;max-width:80ch}.write-up h4{margin:10px 0 2px;font-size:13px}.write-up p{margin:0}.write-up [title]{text-decoration:underline dotted;cursor:help}pre.code{font-family:ui-monospace,monospace;font-size:12px;white-space:pre-wrap;background:var(--bg);border:1px solid var(--line);border-radius:4px;padding:6px 8px;margin:0}.factor .margin,.factor dl.trust{max-width:420px}.verdict{display:flex;flex-wrap:wrap;gap:12px 24px;border:1px solid var(--line);border-left:4px solid var(--mut);background:var(--card);border-radius:6px;padding:10px 14px;margin:8px 0 16px}.verdict.improves{border-left-color:var(--good)}.verdict.does_not_improve{border-left-color:var(--bad)}.verdict.unjudged{display:block;border-left-color:var(--acc)}.verdict h2{font-size:18px;margin:0 0 6px}.verdict h3{font-size:13px;margin:10px 0 4px}.verdict p{margin:4px 0;max-width:80ch}.verdict .validation{flex:2 1 320px;min-width:0}.verdict .holdout{flex:1 1 260px;min-width:0;border-left:1px solid var(--line);padding-left:16px}.verdict .holdout.forced{border:2px solid var(--bad);border-radius:6px;padding:6px 12px}.verdict .warning{color:var(--bad);font-weight:600}.verdict code{font-family:ui-monospace,monospace;font-size:12px}table.comparison{border-collapse:collapse;font-size:12px;font-variant-numeric:tabular-nums}.comparison th,.comparison td{padding:2px 10px 2px 0;text-align:right;white-space:nowrap}.comparison th:first-child{text-align:left;font-weight:400;color:var(--mut)}.comparison thead th{color:var(--mut);font-weight:500}.up{color:var(--good)}.down{color:var(--bad)}ol.opens{margin:2px 0;padding-left:20px}dl.contribution{margin:6px 0 0;font-size:12px}dl.contribution>div{display:flex;gap:8px}dl.contribution dt{color:var(--mut);min-width:56px}dl.contribution dd{margin:0}.diagnosis{border:1px solid var(--line);border-radius:6px;padding:6px 12px;margin:8px 0 14px;max-width:80ch;font-size:13px}.diagnosis h2{font-size:13px;margin:2px 0 4px;color:var(--mut);font-weight:600}.diagnosis ul{list-style:none;margin:0;padding:0}.diagnosis li{padding:4px 0}.diagnosis li+li{border-top:1px solid var(--line)}.diagnosis p{margin:0}.diagnosis .do{color:var(--mut)}.diagnosis.prominent{border-color:var(--bad);border-left-width:4px;background:var(--card);padding:10px 14px;font-size:14px}.diagnosis.prominent h2{font-size:16px;color:var(--fg)}.diagnosis.prominent .seen{font-weight:600}.numbers{display:flex;flex-wrap:wrap;gap:6px 18px;align-items:baseline;margin-bottom:12px}.numbers b{font-size:16px}.numbers .good{color:var(--good)}.trials-page{display:grid;grid-template-columns:minmax(0,1fr) minmax(280px,380px);gap:16px;align-items:start}@media(max-width:900px){.trials-page{grid-template-columns:minmax(0,1fr)}}figure.admission{margin:0}figure.admission svg{width:100%;max-width:760px;height:auto;display:block}.admission .zone{fill:color-mix(in srgb,var(--good) 9%,transparent)}.admission .axis{stroke:var(--line)}.admission .limit{stroke:var(--mut);stroke-dasharray:4 3}.admission .label{fill:var(--mut);font-size:11px}.admission .point{cursor:pointer;stroke:var(--card);stroke-width:1}.admission .point.accepted,.key.accepted{fill:var(--good);color:var(--good)}.admission .point.rejected,.key.rejected{fill:var(--bad);color:var(--bad);fill-opacity:.7}.key.failed{color:var(--fail)}.admission .point.fresh{stroke:var(--acc);stroke-width:3}.admission .point.picked{stroke:var(--fg);stroke-width:2}.never h3{font-size:15px;margin:16px 0 4px}.never h4{font-size:13px;margin:10px 0 2px;font-weight:600}.never button.row{display:block;width:100%;color:var(--fg);padding:3px 6px;border-radius:4px;overflow-wrap:anywhere}.never button.row:hover,.never button.row.picked{background:color-mix(in srgb,var(--acc) 10%,transparent)}.never button.row.sub{padding-left:28px;font-size:12px}aside.reader{border:1px solid var(--line);background:var(--card);border-radius:6px;padding:10px 12px;position:sticky;top:52px;max-height:calc(100vh - 68px);overflow:auto}aside.reader h3{font-size:14px;margin:4px 0 6px;overflow-wrap:anywhere}aside.reader h4{margin:12px 0 2px;font-size:13px}aside.reader p{margin:4px 0}aside.reader .reason{font-size:12px;overflow-wrap:anywhere}aside.reader .hint{font-size:12px;color:var(--mut)}ol.versions{padding-left:18px;margin:4px 0}ol.versions li{margin-bottom:8px}.sorts button.select{margin-left:auto;border-color:var(--acc);color:var(--acc)}.selection h2{font-size:16px}.table-scroll{overflow-x:auto}table.pick{width:100%;border-collapse:collapse;font-size:12px;font-variant-numeric:tabular-nums}table.pick th{text-align:left;font-weight:600;color:var(--mut);border-bottom:1px solid var(--line);padding:4px 6px;white-space:nowrap}table.pick td{padding:4px 6px;border-bottom:1px solid var(--line);vertical-align:top}table.pick td.name{max-width:320px;overflow-wrap:anywhere}table.pick tr:not(.ticked):not(.opened) td{opacity:.5}table.pick tr.opened td{background:var(--card)}table.pick tr.opened h4{margin:6px 0 2px;font-size:12px}.holdout-state{padding:6px 10px;border-left:3px solid var(--line);margin:6px 0}.holdout-state.closed{border-left-color:var(--good)}.holdout-state.opened{border-left-color:var(--bad);color:var(--bad);font-weight:600}.calls{position:relative}.calls button{position:absolute;top:6px;right:6px;font:inherit;font-size:12px;padding:2px 9px;border-radius:9px;border:1px solid var(--line);background:var(--card);color:var(--fg);cursor:pointer}.health{margin:0 0 12px;padding:6px 10px;border:1px solid var(--line);border-radius:6px;background:var(--card)}.health p{margin:2px 0}.health .warn,.health b.warn{color:var(--bad)}.health details summary{cursor:pointer;color:var(--mut)}table.tokens{border-collapse:collapse;margin:4px 0}table.tokens th,table.tokens td{text-align:right;padding:1px 8px;border-bottom:1px solid var(--line)}table.tokens th{color:var(--mut);font-weight:500}section.round{border:1px solid var(--line);background:var(--card);border-radius:6px;padding:8px 12px;margin-bottom:12px}section.round h3{font-size:14px;margin:0 0 6px}.timeline .lane{display:flex;align-items:flex-start;gap:8px;padding:2px 0}.timeline .role{width:72px;flex:none;font-size:12px;line-height:16px;color:var(--mut)}.timeline .rows{flex:1;display:flex;flex-direction:column;gap:2px}.timeline .track{position:relative;height:16px;border-bottom:1px solid var(--line)}.timeline .mark{position:absolute;top:1px;height:14px;padding:0;border:0;border-radius:3px;background:var(--acc);cursor:pointer}.timeline .mark.out{background:color-mix(in srgb,var(--acc) 30%,transparent);outline:1px dashed var(--acc);cursor:default}.timeline .mark.failed{background:var(--bad);cursor:default}aside.reader .back{display:block;margin-bottom:6px}.timeline .mark.picked{outline:2px solid var(--fg);outline-offset:1px}.timeline .mark.evaluating{background:color-mix(in srgb,var(--good) 45%,transparent);cursor:default}.timeline .axis{display:flex;justify-content:space-between;margin-left:80px}details.pending>summary{cursor:pointer;margin-top:6px}details.pending .cards{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}details.pending .card{display:flex;flex-direction:column;align-items:flex-start;gap:1px;min-width:120px;padding:4px 8px;font:inherit;font-size:12px;color:var(--fg);background:var(--bg);border:1px solid var(--line);border-left:3px solid var(--mut);border-radius:4px;cursor:pointer;text-align:left}details.pending .card.picked{outline:2px solid var(--acc)}.st-proposed,.st-fixed,.st-rewritten{border-left-color:var(--acc)!important}.st-consistent{border-left-color:var(--good)!important}.st-inconsistent{border-left-color:var(--bad)!important}.card .stage,.badge.st-planned{color:var(--mut)}.badge.st-proposed,.badge.st-fixed,.badge.st-rewritten,.card.st-proposed .stage,.card.st-fixed .stage,.card.st-rewritten .stage{color:var(--acc)}.badge.st-consistent,.card.st-consistent .stage{color:var(--good)}.badge.st-inconsistent,.card.st-inconsistent .stage{color:var(--bad)}.badge[class*=st-]{border-left-color:currentColor!important}aside.reader .flag{color:var(--bad);font-weight:600;font-size:12px}aside.reader .good{color:var(--good)}aside.reader .bad{color:var(--bad)}aside.reader pre.code{white-space:pre-wrap;overflow-wrap:anywhere;max-height:50vh;overflow:auto}.pages button.pause{margin-left:auto;font:inherit;font-size:12px;padding:2px 10px;border-radius:9px;border:1px solid var(--line);background:var(--card);color:var(--fg);cursor:pointer}.pages button.pause.on{background:var(--acc);border-color:var(--acc);color:var(--bg)}.paused{position:sticky;top:41px;z-index:2;margin:0 0 12px;padding:6px 10px;border:1px solid var(--acc);border-radius:6px;background:color-mix(in srgb,var(--acc) 12%,var(--bg))}pre.yaml div{white-space:pre-wrap;padding-left:4.8em;text-indent:-4.8em}.y-no{display:inline-block;width:2.6em;margin-right:.8em;text-align:right;color:var(--mut);opacity:.6;-webkit-user-select:none;user-select:none;text-indent:0}.y-key{color:#2557d6}.y-colon{color:var(--mut)}.y-dash{color:#a16207}.y-string{color:#1f8a4c}.y-number{color:#b45309}.y-bool,.y-null{color:#9333ea}.y-flow{color:#0e7490}.y-block{color:var(--mut)}.y-comment{color:var(--mut);font-style:italic}@media(prefers-color-scheme:dark){.y-key{color:#7aa2ff}.y-string{color:#86d29b}.y-number{color:#f0a868}.y-bool,.y-null{color:#c4a2ff}.y-flow{color:#67c7d8}.y-dash{color:#e0b15a}}.y-fold{display:inline-block;width:1.2em;margin-right:.2em;padding:0;border:0;background:none;color:var(--mut);font:inherit;text-indent:0;cursor:pointer}span.y-fold{cursor:default}.y-fold:hover{color:var(--fg)}.y-folded{margin-left:.6em;padding:0 6px;border:1px solid var(--line);border-radius:4px;background:var(--card);color:var(--mut);font:inherit;font-size:11px;text-indent:0;cursor:pointer}.y-tools{display:flex;gap:8px;justify-content:flex-end;margin:0 0 4px}.y-tools button{padding:1px 8px;border:1px solid var(--line);border-radius:9px;background:var(--card);color:var(--fg);font:inherit;cursor:pointer}.files{display:grid;grid-template-columns:260px minmax(0,1fr);gap:16px;align-items:start}.files nav{display:grid;gap:4px;position:sticky;top:60px}.files nav button{display:grid;gap:2px;text-align:left;padding:8px 10px;background:none;border:1px solid transparent;border-radius:8px;color:var(--fg);font:inherit;cursor:pointer}.files nav button.here{background:var(--card);border-color:var(--line)}.files section h3{margin-top:0}@media(max-width:760px){.files{grid-template-columns:1fr}.files nav{position:static}}.checks{display:flex;flex-wrap:wrap;gap:6px;margin:4px 0 10px}.check{display:inline-flex;gap:5px;align-items:baseline;padding:2px 9px;border-radius:9px;border:1px solid currentColor;font-size:12px}.check .bar{color:var(--mut);font-size:11px}.check.pass{color:var(--good);background:color-mix(in srgb,var(--good) 10%,transparent)}.check.fail{color:var(--bad);background:color-mix(in srgb,var(--bad) 10%,transparent)}.check.unknown{color:var(--mut)}figure.chart{margin:6px 0 14px}figure.chart figcaption{font-size:12px;margin-bottom:4px}figure.chart svg{display:block;max-width:520px;overflow:visible}figure.chart .axis{stroke:var(--mut);stroke-width:1}figure.chart .grid{stroke:var(--line);stroke-width:1}figure.chart .tick{fill:var(--mut);font-size:9px;text-anchor:middle}figure.chart .tick.start{text-anchor:start}figure.chart rect.up{fill:var(--good)}figure.chart rect.down{fill:var(--bad)}figure.chart polyline{fill:none;stroke-width:1.6}figure.chart polyline.ic{stroke:var(--acc)}figure.chart polyline.partial{stroke:#b7791f;stroke-dasharray:5 3}figure.chart .key{font-weight:600}figure.chart .key.ic{color:var(--acc)}figure.chart .key.partial{color:#b7791f}
|