loopmath 0.2.3__py3-none-any.whl → 0.2.4__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.
- loopmath/__init__.py +1 -1
- loopmath/adapters/atrium.py +1 -1
- loopmath/adapters/bb.py +1 -1
- loopmath/adapters/opencode.py +1 -1
- loopmath/adapters/orca.py +1 -1
- loopmath/adapters/otel_genai.py +1 -1
- loopmath/adapters/paseo.py +1 -1
- loopmath/belief/check_pymc.py +7 -3
- loopmath/belief/commands.py +41 -1
- loopmath/belief/fit.py +1 -1
- loopmath/builder/context.py +2 -1
- loopmath/builder/predict.py +8 -1
- loopmath/cli.py +73 -30
- loopmath/cli_graph.py +28 -19
- loopmath/cli_registry.py +25 -8
- loopmath/cli_support.py +117 -4
- loopmath/e0/verb.py +16 -8
- loopmath/fit_bayes.py +57 -0
- loopmath/graph/dataset_build.py +2 -2
- loopmath/graph/html_common.py +56 -25
- loopmath/graph/html_css.py +5 -2
- loopmath/graph/html_data.py +35 -12
- loopmath/graph/html_force.py +9 -1
- loopmath/graph/html_render.py +1 -1
- loopmath/graph/html_swimlanes.py +28 -6
- loopmath/logmatch/clip.py +16 -3
- loopmath/logmatch/match.py +94 -7
- loopmath/logmatch/settle.py +7 -6
- loopmath/onboard/commands.py +19 -6
- loopmath/onboard/history.py +48 -6
- loopmath/onboard/usual.py +4 -2
- loopmath/output.py +47 -0
- loopmath/priors/commands.py +5 -3
- loopmath/recommend/commands.py +65 -18
- loopmath/recommend/message.py +4 -3
- loopmath/recommend/storeread.py +23 -10
- loopmath/report/terminal.py +28 -3
- loopmath/share/commands.py +45 -5
- loopmath/share/export.py +12 -3
- loopmath/skill/commands.py +34 -7
- loopmath/skill/doctor.py +9 -2
- loopmath/skill/install.py +38 -21
- loopmath/skill/skills/loopmath-record-run/SKILL.md +2 -2
- loopmath/skill/skills/reference.md +10 -1
- loopmath/store/budget.py +79 -12
- loopmath/store/commands.py +166 -47
- loopmath/store/config.py +43 -0
- loopmath/store/finish.py +2 -2
- loopmath/store/fitjob.py +56 -11
- loopmath/store/home.py +68 -11
- loopmath/store/ids.py +28 -4
- loopmath/store/record.py +192 -31
- loopmath/store/report.py +18 -14
- loopmath/store/run_names.py +45 -0
- loopmath/store/runs.py +14 -0
- loopmath/store/status.py +42 -10
- loopmath/store/verify.py +106 -0
- loopmath/surface_presentation.py +28 -2
- loopmath/views/assets/builder.js +29 -6
- loopmath/views/assets/common.js +14 -4
- loopmath/views/assets/estimates.js +10 -5
- loopmath/views/assets/graph.js +71 -10
- loopmath/views/assets/plans.js +15 -4
- loopmath/views/assets/results.js +49 -19
- loopmath/views/assets/runs.js +1 -1
- loopmath/views/assets/viz.css +3 -1
- loopmath/views/assets/viz.js +19 -3
- loopmath/views/common.py +25 -1
- loopmath/views/posterior.py +126 -7
- loopmath/views/runs.py +37 -11
- loopmath/workflows/commands.py +14 -4
- loopmath/workflows/shapes.py +15 -0
- {loopmath-0.2.3.dist-info → loopmath-0.2.4.dist-info}/METADATA +37 -24
- {loopmath-0.2.3.dist-info → loopmath-0.2.4.dist-info}/RECORD +81 -79
- {loopmath-0.2.3.data → loopmath-0.2.4.data}/data/spec/ocp-v0.2.schema.json +0 -0
- {loopmath-0.2.3.data → loopmath-0.2.4.data}/data/spec/ocp-v0.schema.json +0 -0
- {loopmath-0.2.3.data → loopmath-0.2.4.data}/data/spec/ocp_conformance.py +0 -0
- {loopmath-0.2.3.dist-info → loopmath-0.2.4.dist-info}/WHEEL +0 -0
- {loopmath-0.2.3.dist-info → loopmath-0.2.4.dist-info}/entry_points.txt +0 -0
- {loopmath-0.2.3.dist-info → loopmath-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {loopmath-0.2.3.dist-info → loopmath-0.2.4.dist-info}/top_level.txt +0 -0
loopmath/__init__.py
CHANGED
loopmath/adapters/atrium.py
CHANGED
|
@@ -470,7 +470,7 @@ class AtriumAdapter(Adapter):
|
|
|
470
470
|
return {
|
|
471
471
|
"ocp": "0.2",
|
|
472
472
|
"producer": {
|
|
473
|
-
"name": "
|
|
473
|
+
"name": "loopmath/adapter-atrium",
|
|
474
474
|
"framework": "atrium",
|
|
475
475
|
"source_contract": "atrium/timeline+task-run-segments",
|
|
476
476
|
"capabilities": {
|
loopmath/adapters/bb.py
CHANGED
|
@@ -408,7 +408,7 @@ class BbAdapter(Adapter):
|
|
|
408
408
|
)
|
|
409
409
|
ignored_snapshots = sum(thread["ignored_token_snapshots"] for thread in selected)
|
|
410
410
|
producer: dict[str, Any] = {
|
|
411
|
-
"name": "
|
|
411
|
+
"name": "loopmath/adapter-bb",
|
|
412
412
|
"framework": "bb",
|
|
413
413
|
"source_contract": "bb/sqlite",
|
|
414
414
|
"capabilities": {
|
loopmath/adapters/opencode.py
CHANGED
|
@@ -386,7 +386,7 @@ def _emit_document(
|
|
|
386
386
|
else:
|
|
387
387
|
source_contract = "opencode export"
|
|
388
388
|
producer: dict[str, Any] = {
|
|
389
|
-
"name": "
|
|
389
|
+
"name": "loopmath/adapter-opencode",
|
|
390
390
|
"framework": "opencode",
|
|
391
391
|
"source_contract": source_contract,
|
|
392
392
|
"capabilities": {
|
loopmath/adapters/orca.py
CHANGED
loopmath/adapters/otel_genai.py
CHANGED
|
@@ -383,7 +383,7 @@ class OtelGenAIAdapter(Adapter):
|
|
|
383
383
|
return {
|
|
384
384
|
"ocp": "0.2",
|
|
385
385
|
"producer": {
|
|
386
|
-
"name": "
|
|
386
|
+
"name": "loopmath/adapter-otel-genai",
|
|
387
387
|
"framework": "+".join(harnesses) if harnesses else "unknown",
|
|
388
388
|
"source_contract": "OTLP/JSON file exporter",
|
|
389
389
|
"capabilities": {
|
loopmath/adapters/paseo.py
CHANGED
loopmath/belief/check_pymc.py
CHANGED
|
@@ -21,9 +21,13 @@ from __future__ import annotations
|
|
|
21
21
|
import time
|
|
22
22
|
|
|
23
23
|
import numpy as np
|
|
24
|
-
|
|
25
|
-
import
|
|
26
|
-
|
|
24
|
+
|
|
25
|
+
from ..fit_bayes import use_store_compiledir
|
|
26
|
+
|
|
27
|
+
use_store_compiledir() # before pytensor is imported: its compile folder goes in the store's cache
|
|
28
|
+
import pymc as pm # noqa: E402
|
|
29
|
+
import pytensor # noqa: E402
|
|
30
|
+
import pytensor.tensor as pt # noqa: E402
|
|
27
31
|
|
|
28
32
|
from .gaussian import HYPER_SD, SIGMA_PRIOR_SD, GaussianHead
|
|
29
33
|
|
loopmath/belief/commands.py
CHANGED
|
@@ -121,6 +121,36 @@ def _print_summary(s: dict) -> None:
|
|
|
121
121
|
f"(table in pymc_check.json)")
|
|
122
122
|
|
|
123
123
|
|
|
124
|
+
def _in_pymc_check(exc: BaseException) -> bool:
|
|
125
|
+
"""Whether `exc` was raised inside the `--full` PyMC check (check_pymc, pymc or pytensor)."""
|
|
126
|
+
tb = exc.__traceback__
|
|
127
|
+
while tb is not None:
|
|
128
|
+
if tb.tb_frame.f_globals.get("__name__") == "loopmath.belief.check_pymc":
|
|
129
|
+
return True
|
|
130
|
+
tb = tb.tb_next
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _pymc_check_failed(home: Path, exc: BaseException) -> int:
|
|
135
|
+
"""`fit --full` whose PyMC check failed (a compiler error, say): remove the unfinished fit
|
|
136
|
+
folder and say what happened in one line, without a traceback."""
|
|
137
|
+
from .fit import FitBusy, fit_lock, remove_partials
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
with fit_lock(home, LOCK_WAIT_S): # under the lock every .partial is from a fit that stopped
|
|
141
|
+
remove_partials(home)
|
|
142
|
+
except FitBusy:
|
|
143
|
+
pass # the next fit removes it
|
|
144
|
+
lines = [ln.strip() for ln in str(exc).splitlines() if ln.strip()]
|
|
145
|
+
said = ([ln for ln in lines if ln.startswith("ld:")] # the linker's reason, then the first compiler error
|
|
146
|
+
or [ln for ln in lines if ln.startswith("error:") or " error: " in ln])
|
|
147
|
+
detail = (said[0] if said else (lines or [type(exc).__name__])[-1])[:200]
|
|
148
|
+
kind = "could not compile its model on this machine" if type(exc).__name__ == "CompileError" else "stopped"
|
|
149
|
+
return output.fail(f"fit --full: the PyMC check {kind} ({type(exc).__name__}: {detail}). No fit was "
|
|
150
|
+
"written and fits/latest did not move; loopmath fit without --full fits as usual.",
|
|
151
|
+
output.EXIT_USER)
|
|
152
|
+
|
|
153
|
+
|
|
124
154
|
def fit(args: argparse.Namespace) -> int:
|
|
125
155
|
home = output.home(getattr(args, "home", None))
|
|
126
156
|
without = tuple(getattr(args, "without", None) or ())
|
|
@@ -140,10 +170,16 @@ def fit(args: argparse.Namespace) -> int:
|
|
|
140
170
|
else:
|
|
141
171
|
print(f"fit queued: {started.get('reason', 'a fit is running')}")
|
|
142
172
|
return output.EXIT_OK
|
|
173
|
+
import contextlib
|
|
174
|
+
import sys
|
|
175
|
+
|
|
143
176
|
from .fit import FitBusy, NothingToFit, UnknownSource, fit as run_fit
|
|
144
177
|
|
|
178
|
+
# --full: a failed compile makes pytensor print where it left the C code on stdout, which is for the result
|
|
179
|
+
quiet = contextlib.redirect_stdout(sys.stderr) if full else contextlib.nullcontext()
|
|
145
180
|
try:
|
|
146
|
-
|
|
181
|
+
with quiet:
|
|
182
|
+
path = run_fit(home, no_prior=no_prior, without=without, full=full, wait_s=LOCK_WAIT_S)
|
|
147
183
|
except FitBusy:
|
|
148
184
|
return output.fail(f"another fit has held {home / 'fits'} for more than {int(LOCK_WAIT_S)} s; "
|
|
149
185
|
"try again later or use --background", output.EXIT_LOCKED)
|
|
@@ -151,6 +187,10 @@ def fit(args: argparse.Namespace) -> int:
|
|
|
151
187
|
return output.fail(str(exc), output.EXIT_NOT_FOUND)
|
|
152
188
|
except NothingToFit as exc:
|
|
153
189
|
return output.fail(str(exc), output.EXIT_USER)
|
|
190
|
+
except Exception as exc:
|
|
191
|
+
if not (full and _in_pymc_check(exc)):
|
|
192
|
+
raise
|
|
193
|
+
return _pymc_check_failed(home, exc)
|
|
154
194
|
summary = _summary(path)
|
|
155
195
|
if as_json:
|
|
156
196
|
output.emit_json(SCHEMA, summary)
|
loopmath/belief/fit.py
CHANGED
|
@@ -72,7 +72,7 @@ REASON_NOTES = {
|
|
|
72
72
|
"attempt without usable cost": "crashed or no usage reported",
|
|
73
73
|
"attempt outside the workflow": "a model attempt at no piece of the run's workflow",
|
|
74
74
|
"duplicate run id": "the same run found twice",
|
|
75
|
-
SHIPPED_COPY: "
|
|
75
|
+
SHIPPED_COPY: "this run ships with loopmath and is also in your store, so it counts once, as yours",
|
|
76
76
|
"no configuration": "the run records no workflow and settings",
|
|
77
77
|
"workflow ref not resolved": "a workflow this version does not know",
|
|
78
78
|
"workflow has no pieces": "an empty workflow",
|
loopmath/builder/context.py
CHANGED
|
@@ -21,6 +21,7 @@ from typing import Any
|
|
|
21
21
|
from ..output import EXIT_NOT_FOUND, EXIT_OK, fail, home as home_dir
|
|
22
22
|
from ..recommend import commands as C
|
|
23
23
|
from ..recommend import engine, storeread
|
|
24
|
+
from ..store import run_names
|
|
24
25
|
from ..recommend.curve import accepted_within
|
|
25
26
|
from ..recommend.engine import Recommendation, r6
|
|
26
27
|
from ..recommend.storeread import Conf
|
|
@@ -221,7 +222,7 @@ def recorded_runs(home: Any) -> list[tuple[Configuration, int]]:
|
|
|
221
222
|
run_of[str(cfg)] = str(row["run"])
|
|
222
223
|
out = []
|
|
223
224
|
for cfg_id, n in counts.items():
|
|
224
|
-
doc = storeread.read_json(home / "runs" /
|
|
225
|
+
doc = storeread.read_json(home / "runs" / run_names.file_name(run_of[cfg_id])) if cfg_id in run_of else None
|
|
225
226
|
conf = doc.get("run", {}).get("configuration") if isinstance(doc, dict) else None
|
|
226
227
|
cfg = storeread.config_from_any(conf) if conf else None
|
|
227
228
|
if cfg is not None:
|
loopmath/builder/predict.py
CHANGED
|
@@ -136,6 +136,12 @@ def clean(obj: dict[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
|
136
136
|
return {**obj, "id": None, "settings": settings}, notes
|
|
137
137
|
|
|
138
138
|
|
|
139
|
+
# P3-21 (0.2.4): a review gate that names a repairer at a round limit of 1 is one of loopmath's own shapes (reviewed
|
|
140
|
+
# once; the search builds it and may recommend it). The workflow file check warns about it; the builder page instead
|
|
141
|
+
# says on the gate what 1 round means, so that warning is not shown here.
|
|
142
|
+
REVIEW_ONCE = "budget_rounds is 1"
|
|
143
|
+
|
|
144
|
+
|
|
139
145
|
def parse_config(obj: Any, models: dict[str, int]
|
|
140
146
|
) -> tuple[Configuration | None, list[dict[str, Any]], list[dict[str, Any]]]:
|
|
141
147
|
"""(configuration, errors, warnings); the configuration is None when it does not read at all."""
|
|
@@ -158,7 +164,8 @@ def parse_config(obj: Any, models: dict[str, int]
|
|
|
158
164
|
errors.append(err(p.id, f"piece {p.id!r}: model {s.model!r} is a retired or unknown model; "
|
|
159
165
|
f"pick one of {known}"))
|
|
160
166
|
if not errors:
|
|
161
|
-
|
|
167
|
+
shown = [w for w in workflow_warnings(cfg.workflow) if REVIEW_ONCE not in w]
|
|
168
|
+
warnings += notes_of(shown + settings_warnings(cfg.settings), cfg)
|
|
162
169
|
return cfg, errors, warnings
|
|
163
170
|
|
|
164
171
|
|
loopmath/cli.py
CHANGED
|
@@ -5,27 +5,26 @@ Verbs:
|
|
|
5
5
|
scan explain where implicit scanning occurs
|
|
6
6
|
adapt emit OCP v0.2 from a registered ADE adapter
|
|
7
7
|
validate-prices check a price table's schema, staleness and todo flags
|
|
8
|
-
analyze-e0 the frozen E0 corpus walkdown
|
|
9
8
|
prices show the active price table
|
|
10
9
|
graph the workflow graph of one or more workspaces (ocp, json, dot, run, html: self-contained interactive HTML viewer)
|
|
11
|
-
research
|
|
12
|
-
verify-receipts
|
|
10
|
+
research research data folders: research fit, research transfer-test (the bayes extra), research analyze-e0 (the e0 extra)
|
|
11
|
+
verify-receipts check that the receipts in your store are whole and scored right (or give a research ledger file)
|
|
13
12
|
|
|
14
13
|
0.1.0 verbs (design/0.1/02-commands.md, registered in cli_registry.py):
|
|
15
|
-
plan: task-types workflows recommend plan
|
|
16
|
-
record: run start|attempt|artifact|finish|
|
|
14
|
+
plan: task-types workflows recommend builder plan
|
|
15
|
+
record: run start|record|import|attempt|artifact|finish|sessions outcome budget config
|
|
17
16
|
learn: fit onboard share
|
|
18
17
|
view: runs posterior status report doctor skill ocp
|
|
19
18
|
|
|
20
|
-
No verb in this CLI
|
|
21
|
-
($LOOPMATH_HOME, default ~/.loopmath, which also
|
|
22
|
-
|
|
19
|
+
No verb in this CLI connects to another machine; builder serves on 127.0.0.1 only.
|
|
20
|
+
Verbs write only to the store ($LOOPMATH_HOME, default ~/.loopmath, which also
|
|
21
|
+
holds the parse cache and the fit --full compile folder), to the skill folders
|
|
22
|
+
skill install names, and to paths the user names.
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
from __future__ import annotations
|
|
26
26
|
|
|
27
27
|
import argparse
|
|
28
|
-
import importlib.util as _importlib_util
|
|
29
28
|
import sys
|
|
30
29
|
import time
|
|
31
30
|
from types import ModuleType
|
|
@@ -79,6 +78,7 @@ class _CliModule(ModuleType):
|
|
|
79
78
|
sys.modules[__name__].__class__ = _CliModule
|
|
80
79
|
|
|
81
80
|
DEFAULT_SINCE_DAYS = 14.0
|
|
81
|
+
_ANALYZE_SCHEMA = "loopmath.analyze/1"
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
def _counted_noun(count: int, singular: str, plural: str | None = None) -> str:
|
|
@@ -185,6 +185,18 @@ def analyze(args: argparse.Namespace) -> int:
|
|
|
185
185
|
ci=args.ci,
|
|
186
186
|
)
|
|
187
187
|
result["ratio_pair"] = _extremes_ratio(df, result, price_mod)
|
|
188
|
+
result["min_n"] = args.min_n
|
|
189
|
+
|
|
190
|
+
if getattr(args, "json", False):
|
|
191
|
+
from .output import emit_json
|
|
192
|
+
|
|
193
|
+
grading = list(grade_mod.grading_report(records, coverage)) if args.grading else None
|
|
194
|
+
emit_json(_ANALYZE_SCHEMA, _cli_support.analyze_payload(
|
|
195
|
+
result, diag=diag, coverage=coverage, price_lines=price_lines, grading=grading,
|
|
196
|
+
walkdown=surface_mod.walkdown(result)))
|
|
197
|
+
if not args.quiet:
|
|
198
|
+
print(f"ran in {time.time() - t0:.1f} s", file=sys.stderr)
|
|
199
|
+
return 0
|
|
188
200
|
|
|
189
201
|
print(
|
|
190
202
|
terminal.render(
|
|
@@ -246,7 +258,7 @@ def _add_graph(sub) -> None:
|
|
|
246
258
|
p.add_argument("--workspace", action="append", default=[], help="native workspace name to include (repeatable; required unless --ocp is used)")
|
|
247
259
|
p.add_argument("--ocp", action="append", default=[], metavar="FILE", help="read an OCP document (v0.2 or later) as a graph source (repeatable)")
|
|
248
260
|
p.add_argument("--format", type=_cli_support._graph_format, choices=GRAPH_FORMATS, default="ocp", help="ocp (OCP v0.3, default), json (internal dagr_graph), dot (Graphviz), run (herdr-dagr contract v3 run file), html (self-contained interactive HTML viewer)")
|
|
249
|
-
p.add_argument("--out", default=None, help="write here instead of stdout (
|
|
261
|
+
p.add_argument("--out", default=None, help="write here instead of stdout (any path; missing folders are created; an existing file is replaced)")
|
|
250
262
|
p.add_argument("--privacy", choices=("metadata_only", "full"), default="metadata_only", help="OCP privacy profile (default metadata_only)")
|
|
251
263
|
p.add_argument("--logs", default=None, help="parse this path instead of the default log roots")
|
|
252
264
|
p.add_argument(
|
|
@@ -259,7 +271,8 @@ def _add_graph(sub) -> None:
|
|
|
259
271
|
p.add_argument("--limit", type=int, default=None, help="stop after N files per harness")
|
|
260
272
|
p.add_argument("--no-cache", action="store_true", help="reparse everything, ignoring the cache")
|
|
261
273
|
p.add_argument("--prices", default=None, help="price table to use instead of the packaged one")
|
|
262
|
-
p.add_argument("--quiet", action="store_true", help="suppress progress output on stderr
|
|
274
|
+
p.add_argument("--quiet", action="store_true", help="suppress progress output on stderr")
|
|
275
|
+
p.add_argument("--verbose", action="store_true", help="also print every count of what was skipped, excluded or could not be placed")
|
|
263
276
|
p.set_defaults(func=graph_verb)
|
|
264
277
|
|
|
265
278
|
|
|
@@ -279,7 +292,7 @@ def adapt_verb(args: argparse.Namespace) -> int:
|
|
|
279
292
|
|
|
280
293
|
out_path = None
|
|
281
294
|
if args.out:
|
|
282
|
-
out_path, err = _resolve_out(args.out)
|
|
295
|
+
out_path, err = _resolve_out(args.out, "out.ocp.json")
|
|
283
296
|
if err:
|
|
284
297
|
print(err, file=sys.stderr)
|
|
285
298
|
return 2
|
|
@@ -302,9 +315,18 @@ def adapt_verb(args: argparse.Namespace) -> int:
|
|
|
302
315
|
|
|
303
316
|
adapter = adapter_type()
|
|
304
317
|
adapter.configure_services(default_adapter_services())
|
|
305
|
-
|
|
318
|
+
try:
|
|
319
|
+
doc = adapter.emit(selection)
|
|
320
|
+
except (OSError, ValueError) as exc: # a store path it cannot read: say so, no traceback
|
|
321
|
+
print(f"error: adapt {args.name}: {exc}", file=sys.stderr)
|
|
322
|
+
return 1
|
|
306
323
|
if not isinstance(doc, dict):
|
|
307
324
|
raise TypeError(f"adapter {args.name!r} emit() must return a dict")
|
|
325
|
+
if not doc.get("nodes") and not doc.get("attempts"):
|
|
326
|
+
where = ", ".join(str(p) for p in selection.stores) if selection.stores else "its default location"
|
|
327
|
+
print(f"error: adapt {args.name} found no sessions in {where}; nothing written. Point --store at the "
|
|
328
|
+
f"{args.name} data, or widen --since, --until, --session and --workspace", file=sys.stderr)
|
|
329
|
+
return 1
|
|
308
330
|
from .ocp import version_at_least
|
|
309
331
|
|
|
310
332
|
if not version_at_least(doc, "0.2"):
|
|
@@ -323,7 +345,7 @@ def adapt_verb(args: argparse.Namespace) -> int:
|
|
|
323
345
|
def _add_adapt(sub) -> None:
|
|
324
346
|
p = sub.add_parser("adapt", help="emit OCP v0.2 from an ADE store (loopmath ocp migrate converts it to v0.3)")
|
|
325
347
|
p.add_argument("name", metavar="NAME", help="registered adapter name")
|
|
326
|
-
p.add_argument("--out", default=None, help="write here instead of stdout (
|
|
348
|
+
p.add_argument("--out", default=None, help="write here instead of stdout (any path; missing folders are created; an existing file is replaced)")
|
|
327
349
|
p.add_argument("--store", action="append", default=[], metavar="PATH", help="use this store path instead of discovery (repeatable)")
|
|
328
350
|
p.add_argument("--session", action="append", default=[], metavar="ID", help="include this session id (repeatable; default: all)")
|
|
329
351
|
p.add_argument("--workspace", action="append", default=[], metavar="WORKSPACE", help="include this workspace (repeatable; default: all)")
|
|
@@ -371,6 +393,9 @@ def _add_analyze(sub) -> None:
|
|
|
371
393
|
p.add_argument("--tokens", action="store_true", help="rank by output tokens instead of dollars")
|
|
372
394
|
p.add_argument("--grading", action="store_true", help="print which rule graded what")
|
|
373
395
|
p.add_argument("--quiet", action="store_true", help="suppress progress and timing on stderr")
|
|
396
|
+
p.add_argument("--home", default=None, metavar="PATH", help="store root whose cache/ holds the log parse cache "
|
|
397
|
+
"(default: $LOOPMATH_HOME or ~/.loopmath; LOOPMATH_CACHE_DIR wins); analyze writes nothing else")
|
|
398
|
+
p.add_argument("--json", action="store_true", help="print one JSON object on stdout")
|
|
374
399
|
p.set_defaults(func=analyze)
|
|
375
400
|
|
|
376
401
|
|
|
@@ -385,11 +410,18 @@ def _add_validate_prices(sub) -> None:
|
|
|
385
410
|
def _add_fit(sub) -> None:
|
|
386
411
|
from .research_defaults import E1A_OBSERVE, SEED
|
|
387
412
|
|
|
388
|
-
research = sub.add_parser(
|
|
413
|
+
research = sub.add_parser(
|
|
414
|
+
"research",
|
|
415
|
+
help="verbs for research data folders that are not part of the package (loopmath research --help)",
|
|
416
|
+
description="Verbs for research data folders that are not part of the package: sweep run records "
|
|
417
|
+
"(research fit, research transfer-test; they need the bayes extra) and a session corpus "
|
|
418
|
+
"(research analyze-e0; it needs the e0 extra). Name a folder with --sweep-dir or --corpus, "
|
|
419
|
+
"LOOPMATH_SWEEP_DIR or LOOPMATH_E0_CORPUS, or config research.sweep_dir or research.e0_corpus.",
|
|
420
|
+
)
|
|
389
421
|
rsub = research.add_subparsers(dest="research_command", required=True)
|
|
390
422
|
for name, fn, helptext in (
|
|
391
|
-
("fit", fit_verb, "fit the
|
|
392
|
-
("transfer-test", transfer_test_verb, "score held-out
|
|
423
|
+
("fit", fit_verb, "fit the research cost model on sweep run records"),
|
|
424
|
+
("transfer-test", transfer_test_verb, "fit on some cells of the sweep and score the held-out ones"),
|
|
393
425
|
):
|
|
394
426
|
p = rsub.add_parser(name, help=helptext)
|
|
395
427
|
p.add_argument("--sweep-dir", default=None, help="sweep run records directory (default: LOOPMATH_SWEEP_DIR, then research.sweep_dir in config)")
|
|
@@ -405,10 +437,20 @@ def _add_fit(sub) -> None:
|
|
|
405
437
|
else:
|
|
406
438
|
p.add_argument("--ci", type=float, default=0.80, help="central interval for held-out coverage (default 0.80)")
|
|
407
439
|
p.set_defaults(func=fn)
|
|
440
|
+
from .e0 import verb as _e0_verb
|
|
441
|
+
|
|
442
|
+
_e0_verb.register(rsub)
|
|
408
443
|
|
|
409
444
|
|
|
410
445
|
def verify_receipts_verb(args: argparse.Namespace) -> int:
|
|
411
|
-
"""
|
|
446
|
+
"""No ledger: check the store's receipts. A ledger path: verify that research receipt ledger."""
|
|
447
|
+
if args.ledger is None:
|
|
448
|
+
from .store.home import Store
|
|
449
|
+
from .store.verify import verify_lines, verify_store
|
|
450
|
+
|
|
451
|
+
res = verify_store(Store(args.home))
|
|
452
|
+
print("\n".join(verify_lines(res)))
|
|
453
|
+
return 1 if res["failed"] else 0
|
|
412
454
|
from .receipts import ReceiptViolation, verify_receipts
|
|
413
455
|
|
|
414
456
|
try:
|
|
@@ -421,18 +463,20 @@ def verify_receipts_verb(args: argparse.Namespace) -> int:
|
|
|
421
463
|
|
|
422
464
|
|
|
423
465
|
def _add_verify_receipts(sub) -> None:
|
|
424
|
-
p = sub.add_parser("verify-receipts", help="
|
|
466
|
+
p = sub.add_parser("verify-receipts", help="check that the receipts in your store are whole and scored right "
|
|
467
|
+
"(or give a research ledger file)")
|
|
425
468
|
p.add_argument(
|
|
426
469
|
"ledger",
|
|
427
470
|
nargs="?",
|
|
428
|
-
default=
|
|
429
|
-
help="receipt ledger (
|
|
471
|
+
default=None,
|
|
472
|
+
help="a research receipt ledger to verify instead (e.g. e2-receipts.jsonl)",
|
|
430
473
|
)
|
|
431
474
|
p.add_argument(
|
|
432
475
|
"--frozen",
|
|
433
476
|
default=None,
|
|
434
|
-
help="checksum file (default: FROZEN.txt beside the ledger)",
|
|
477
|
+
help="with a ledger: its checksum file (default: FROZEN.txt beside the ledger)",
|
|
435
478
|
)
|
|
479
|
+
p.add_argument("--home", default=None, metavar="PATH", help="the store (default: $LOOPMATH_HOME or ~/.loopmath)")
|
|
436
480
|
p.set_defaults(func=verify_receipts_verb)
|
|
437
481
|
|
|
438
482
|
|
|
@@ -455,15 +499,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
455
499
|
|
|
456
500
|
_registry.register(sub)
|
|
457
501
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
from .e0 import verb as _e0_verb
|
|
502
|
+
# The E0 walkdown draws figures, so running it needs matplotlib (the `e0`
|
|
503
|
+
# extra; SPEC section 2's light core is numpy and pandas). It is listed under
|
|
504
|
+
# `loopmath research`; the old top-level name stays callable, off the list.
|
|
505
|
+
# It registers from e0/verb.py without importing the walkdown, so no other
|
|
506
|
+
# command pays for pandas and matplotlib.
|
|
507
|
+
from .e0 import verb as _e0_verb
|
|
465
508
|
|
|
466
|
-
|
|
509
|
+
_e0_verb.register(sub, listed=False)
|
|
467
510
|
|
|
468
511
|
try:
|
|
469
512
|
_add_fit(sub)
|
loopmath/cli_graph.py
CHANGED
|
@@ -10,6 +10,8 @@ from .cli_support import _pipeline_counters, _scan_snapshot_id
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
GRAPH_FORMATS = ("ocp", "json", "dot", "run", "html")
|
|
13
|
+
# The file name a refused directory `--out` suggests, by format.
|
|
14
|
+
_OUT_EXAMPLES = {"ocp": "out.ocp.json", "json": "out.json", "dot": "out.dot", "run": "out.run.json", "html": "out.html"}
|
|
13
15
|
# Graph.meta keys that are totals, not exclusions: printed in the headline, not the list.
|
|
14
16
|
_GRAPH_META_TOTALS = (
|
|
15
17
|
"n_nodes",
|
|
@@ -47,21 +49,21 @@ def _worktree_root():
|
|
|
47
49
|
return Path(r.stdout.strip()).resolve()
|
|
48
50
|
|
|
49
51
|
|
|
50
|
-
def _resolve_out(out: str):
|
|
51
|
-
"""`--out`
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
or (None, error message); nothing is written on
|
|
52
|
+
def _resolve_out(out: str, example: str = "out.json"):
|
|
53
|
+
"""`--out` as the user named it: any path, relative to the current directory,
|
|
54
|
+
with `~` expanded. A path that is an existing directory is refused (writing
|
|
55
|
+
would replace it), and so is one under an existing file, which cannot be a
|
|
56
|
+
folder. Returns (path, None) or (None, error message); nothing is written on
|
|
57
|
+
refusal. Missing parent folders are created when the file is written.
|
|
58
|
+
`example` is the file name the directory refusal suggests."""
|
|
55
59
|
from pathlib import Path
|
|
56
60
|
|
|
57
|
-
root = _worktree_root()
|
|
58
61
|
target = Path(out).expanduser().resolve()
|
|
59
|
-
if
|
|
60
|
-
return None, f"error: --out {out}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return None, f"error: --out {out} resolves to {target}, outside the worktree {root}; the extractor writes only inside it"
|
|
62
|
+
if target.is_dir():
|
|
63
|
+
return None, f"error: --out {out} is a directory; name a file, for example {Path(out) / example}"
|
|
64
|
+
blocker = next((p for p in target.parents if p.exists()), None)
|
|
65
|
+
if blocker is not None and not blocker.is_dir():
|
|
66
|
+
return None, f"error: --out {out} cannot be written: {blocker} is a file, not a folder"
|
|
65
67
|
return target, None
|
|
66
68
|
|
|
67
69
|
|
|
@@ -106,9 +108,9 @@ def graph_verb(args: argparse.Namespace) -> int:
|
|
|
106
108
|
self-contained interactive visualizer. Everything the
|
|
107
109
|
pipeline excluded (files skipped, cut by `--limit` or the time window,
|
|
108
110
|
records ungraded or unpriced) and everything the extractor could not place
|
|
109
|
-
is counted in the graph's meta, which every format carries
|
|
110
|
-
stderr
|
|
111
|
-
|
|
111
|
+
is counted in the graph's meta, which every format carries; `--verbose`
|
|
112
|
+
prints every counter on stderr, and by default only a summary prints.
|
|
113
|
+
`--quiet` silences progress only.
|
|
112
114
|
"""
|
|
113
115
|
import json
|
|
114
116
|
|
|
@@ -125,7 +127,7 @@ def graph_verb(args: argparse.Namespace) -> int:
|
|
|
125
127
|
|
|
126
128
|
out_path = None
|
|
127
129
|
if args.out:
|
|
128
|
-
out_path, err = _resolve_out(args.out)
|
|
130
|
+
out_path, err = _resolve_out(args.out, _OUT_EXAMPLES.get(args.format, "out.json"))
|
|
129
131
|
if err:
|
|
130
132
|
print(err, file=sys.stderr)
|
|
131
133
|
return 2
|
|
@@ -317,24 +319,31 @@ def graph_verb(args: argparse.Namespace) -> int:
|
|
|
317
319
|
f" imported {len(args.ocp)} OCP document(s); "
|
|
318
320
|
"costs and evidence tiers kept as supplied"
|
|
319
321
|
)
|
|
322
|
+
counters: list[str] = []
|
|
320
323
|
for key in sorted(m):
|
|
321
324
|
value = m[key]
|
|
322
325
|
if key.endswith("_reason"):
|
|
323
326
|
continue
|
|
324
327
|
if value is None and f"{key}_reason" in m:
|
|
325
|
-
|
|
328
|
+
counters.append(f" {key}: unknown ({m[f'{key}_reason']})")
|
|
326
329
|
elif (
|
|
327
330
|
isinstance(value, (int, float))
|
|
328
331
|
and not isinstance(value, bool)
|
|
329
332
|
and value
|
|
330
333
|
and key not in _GRAPH_META_TOTALS
|
|
331
334
|
):
|
|
332
|
-
|
|
335
|
+
counters.append(f" {key}: {value}")
|
|
333
336
|
for key in sorted(emitter):
|
|
334
337
|
# The run exporter is a full honesty ledger: print every one of its
|
|
335
338
|
# counters, including zero, so a collision class is never silent.
|
|
336
339
|
if emitter[key] or (args.format == "run" and key.startswith("run.")):
|
|
337
|
-
|
|
340
|
+
counters.append(f" emitter.{key}: {emitter[key]}")
|
|
341
|
+
# The counters are for whoever debugs the extractor; every format carries them in its meta.
|
|
342
|
+
if getattr(args, "verbose", False):
|
|
343
|
+
for line in counters:
|
|
344
|
+
say(line)
|
|
345
|
+
elif counters:
|
|
346
|
+
say(f" {len(counters)} more counts of what was skipped or could not be placed: add --verbose")
|
|
338
347
|
if out_path is not None:
|
|
339
348
|
say(f"wrote {out_path} ({args.format}) in {time.time() - t0:.1f} s")
|
|
340
349
|
return 0
|
loopmath/cli_registry.py
CHANGED
|
@@ -146,7 +146,7 @@ TIER_CHOICES = ["verified", "reported", "heuristic", "asserted"]
|
|
|
146
146
|
|
|
147
147
|
|
|
148
148
|
def _add_recording(sub) -> None:
|
|
149
|
-
p = sub.add_parser("run", help="record a run: start, record, import, attempt, artifact, finish")
|
|
149
|
+
p = sub.add_parser("run", help="record a run: start, record, import, attempt, artifact, finish, sessions")
|
|
150
150
|
rs = p.add_subparsers(dest="run_command", required=True)
|
|
151
151
|
|
|
152
152
|
q = rs.add_parser("start", help="open a run (OCP v0.3 skeleton in the store)")
|
|
@@ -178,7 +178,7 @@ def _add_recording(sub) -> None:
|
|
|
178
178
|
q.add_argument("--model", default=None, metavar="M", help="model name")
|
|
179
179
|
q.add_argument("--effort", default=None, metavar="E", help="reasoning effort")
|
|
180
180
|
q.add_argument("--cwd", default=None, metavar="PATH", help="the folder the agent works in; with no --session its log is matched by this folder, a session that started in the attempt's window, and the model")
|
|
181
|
-
q.add_argument("--session", default=None, metavar="ID", help="session id, or self for the orchestrator's own Claude Code session, counted only inside this attempt's started-at to ended-at window")
|
|
181
|
+
q.add_argument("--session", default=None, metavar="ID", help="session id, or self for the orchestrator's own Claude Code session, counted only inside this attempt's started-at to ended-at window (outside Claude Code: the one Claude Code session in this folder active in the last 10 minutes; run sessions lists the ids)")
|
|
182
182
|
q.add_argument("--round", type=int, default=1, metavar="K", help="round of this piece, 1 for the first try")
|
|
183
183
|
q.add_argument("--cause", default="initial", choices=["initial", "sent_back", "gate_failed", "followup", "other"],
|
|
184
184
|
help="why this attempt was started")
|
|
@@ -225,10 +225,13 @@ def _add_recording(sub) -> None:
|
|
|
225
225
|
"it replaces the rule the recommendation was made for")
|
|
226
226
|
q.add_argument("--session", action="append", default=[], metavar="[PIECE=]ID", help="a session that worked on the run: a "
|
|
227
227
|
"Claude Code session id, a Codex thread id, or self for this Claude Code session (repeatable); "
|
|
228
|
-
"PIECE= names its piece"
|
|
228
|
+
"PIECE= names its piece; run sessions lists the ids. Only the part of a session inside the window "
|
|
229
|
+
"counts (a Codex session counts whole)")
|
|
229
230
|
q.add_argument("--cwd", action="append", default=[], metavar="[PIECE=]PATH", help="a folder an agent with no session id "
|
|
230
231
|
"worked in; its log is matched at finish by folder and time (repeatable)")
|
|
231
|
-
q.add_argument("--since", default=None, metavar="TS", help="when the work began
|
|
232
|
+
q.add_argument("--since", default=None, metavar="TS", help="when the work began (e.g. 2h or a local ISO time); with "
|
|
233
|
+
"--run the default is the run's start")
|
|
234
|
+
q.add_argument("--until", default=None, metavar="TS", help="when the work ended (e.g. 1h or a local ISO time; default now)")
|
|
232
235
|
q.add_argument("--verified", action="append", default=[], metavar="NAME=VALUE", help="a verdict you observed yourself "
|
|
233
236
|
"(pass, fail, accept, reject, error)")
|
|
234
237
|
q.add_argument("--reported", action="append", default=[], metavar="NAME=VALUE", help="a verdict someone reported")
|
|
@@ -240,6 +243,13 @@ def _add_recording(sub) -> None:
|
|
|
240
243
|
_common(q)
|
|
241
244
|
q.set_defaults(slate=None, new_slate=False, func=_lazy("loopmath.store.record:run_record"))
|
|
242
245
|
|
|
246
|
+
q = rs.add_parser("sessions", help="list the recent Claude Code and Codex sessions in this folder, with their ids")
|
|
247
|
+
q.add_argument("--cwd", default=None, metavar="PATH", help="the folder the sessions ran in (default: this one)")
|
|
248
|
+
q.add_argument("--days", type=float, default=7, metavar="N", help="how far back to look (default 7)")
|
|
249
|
+
q.add_argument("--limit", type=int, default=10, metavar="N", help="at most this many, newest first (default 10)")
|
|
250
|
+
_common(q)
|
|
251
|
+
q.set_defaults(func=_lazy("loopmath.store.commands:run_sessions"))
|
|
252
|
+
|
|
243
253
|
p = sub.add_parser("outcome", help="record a verdict, score, late event or pair preference")
|
|
244
254
|
target = p.add_mutually_exclusive_group(required=True)
|
|
245
255
|
target.add_argument("--run", default=None, metavar="RUN", help="the run the signal is about")
|
|
@@ -329,16 +339,16 @@ def _add_learning(sub) -> None:
|
|
|
329
339
|
p = sub.add_parser("share", help="write the shareable part of your store to a file",
|
|
330
340
|
description="Keeps task types, features, workflows, settings, tokens, dollars and outcomes; "
|
|
331
341
|
"repos, tasks and runs only as salted hashes; no titles, paths, commands, session ids or commits.")
|
|
332
|
-
p.add_argument("--out", default=None, metavar="FILE", help="the share file to write
|
|
342
|
+
p.add_argument("--out", default=None, metavar="FILE", help="the share file to write: plain JSON for FILE.json, gzip JSON for FILE.json.gz or any other name (check it first with --preview)")
|
|
333
343
|
p.add_argument("--since", default=None, metavar="D", help="only runs since then: 90d, 4w, 12h or a date")
|
|
334
344
|
p.add_argument("--preview", action="store_true", help="print exactly what would leave; writes no share file")
|
|
335
345
|
_common(p)
|
|
336
346
|
p.set_defaults(func=_lazy("loopmath.share.commands:share"))
|
|
337
347
|
|
|
338
|
-
p = sub.add_parser("prior", help="the shipped prior bundle: show, build, import-shared")
|
|
348
|
+
p = sub.add_parser("prior", help="the shipped prior bundle: show, build, import-shared, remove-shared")
|
|
339
349
|
ps = p.add_subparsers(dest="prior_command", required=True)
|
|
340
350
|
q = ps.add_parser("build", help="rebuild the packaged prior bundle from the sweep, E0 and RQ1 inputs")
|
|
341
|
-
q.add_argument("--out", default=None, metavar="DIR", help="where to write the bundle (
|
|
351
|
+
q.add_argument("--out", default=None, metavar="DIR", help="where to write the bundle (required)")
|
|
342
352
|
q.add_argument("--sweep-dir", action="append", default=None, metavar="PATH", help="sweep results, one folder per batch; repeatable (default: LOOPMATH_SWEEP_DIR, one folder)")
|
|
343
353
|
q.add_argument("--e0-corpus", default=None, metavar="PATH", help="E0 corpus (default: LOOPMATH_E0_CORPUS)")
|
|
344
354
|
q.add_argument("--rq1-dir", default=None, metavar="PATH", help="RQ1 OCP documents (default: LOOPMATH_PRIOR_RQ1)")
|
|
@@ -352,6 +362,10 @@ def _add_learning(sub) -> None:
|
|
|
352
362
|
q.add_argument("file", metavar="FILE", help="a loopmath.share/1 file written by loopmath share (gzip or plain JSON)")
|
|
353
363
|
_common(q)
|
|
354
364
|
q.set_defaults(func=_lazy("loopmath.share.commands:import_shared"))
|
|
365
|
+
q = ps.add_parser("remove-shared", help="take an imported organization's runs out of your priors")
|
|
366
|
+
q.add_argument("org", metavar="ORG", help="the organization import-shared named, as shared:HASH or HASH")
|
|
367
|
+
_common(q)
|
|
368
|
+
q.set_defaults(func=_lazy("loopmath.share.commands:remove_shared"))
|
|
355
369
|
|
|
356
370
|
|
|
357
371
|
# ---------------------------------------------------------------- viewing
|
|
@@ -408,6 +422,9 @@ def _add_viewing(sub) -> None:
|
|
|
408
422
|
help="for you in every folder (user), or for this repository only (project)")
|
|
409
423
|
q.add_argument("--dir", default=None, metavar="PATH", help="the project folder for --scope project "
|
|
410
424
|
"(default: this folder)")
|
|
425
|
+
if verb == "install":
|
|
426
|
+
q.add_argument("--dry-run", action="store_true", help="print every file install would write or "
|
|
427
|
+
"change, and write nothing")
|
|
411
428
|
_common(q)
|
|
412
429
|
q.set_defaults(func=_lazy(f"loopmath.skill.commands:{verb}"))
|
|
413
430
|
q = ss.add_parser("show", help="print one skill (default loopmath, the start-here skill) or the shared reference")
|
|
@@ -429,7 +446,7 @@ def _add_viewing(sub) -> None:
|
|
|
429
446
|
q.set_defaults(func=_lazy("loopmath.ocp.commands:migrate"))
|
|
430
447
|
|
|
431
448
|
|
|
432
|
-
HIDDEN = frozenset({"plan"}) # callable, but not listed by `loopmath --help`
|
|
449
|
+
HIDDEN = frozenset({"plan", "analyze-e0"}) # callable, but not listed by `loopmath --help`
|
|
433
450
|
|
|
434
451
|
|
|
435
452
|
def command_metavar(sub) -> str:
|