alphaengine 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. {alphaengine-0.3.0 → alphaengine-0.4.0}/PKG-INFO +5 -1
  2. {alphaengine-0.3.0 → alphaengine-0.4.0}/README.md +4 -0
  3. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/_version.py +13 -1
  4. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/cli.py +266 -11
  5. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/client/executor.py +81 -17
  6. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/client/session.py +61 -0
  7. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/commands.py +21 -0
  8. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/signals.py +8 -1
  9. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/study/report.py +6 -1
  10. {alphaengine-0.3.0 → alphaengine-0.4.0}/tests/test_cli.py +168 -0
  11. {alphaengine-0.3.0 → alphaengine-0.4.0}/tests/test_client.py +49 -8
  12. {alphaengine-0.3.0 → alphaengine-0.4.0}/tests/test_smoke.py +7 -2
  13. {alphaengine-0.3.0 → alphaengine-0.4.0}/.github/workflows/ci.yml +0 -0
  14. {alphaengine-0.3.0 → alphaengine-0.4.0}/.github/workflows/publish.yml +0 -0
  15. {alphaengine-0.3.0 → alphaengine-0.4.0}/.gitignore +0 -0
  16. {alphaengine-0.3.0 → alphaengine-0.4.0}/LICENSE +0 -0
  17. {alphaengine-0.3.0 → alphaengine-0.4.0}/SECURITY.md +0 -0
  18. {alphaengine-0.3.0 → alphaengine-0.4.0}/pyproject.toml +0 -0
  19. {alphaengine-0.3.0 → alphaengine-0.4.0}/scripts/gen_docs.py +0 -0
  20. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/__init__.py +0 -0
  21. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/__main__.py +0 -0
  22. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/agent/__init__.py +0 -0
  23. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/agent/answer.py +0 -0
  24. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/agent/driver.py +0 -0
  25. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/auth.py +0 -0
  26. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/client/__init__.py +0 -0
  27. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/client/agent.py +0 -0
  28. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/__init__.py +0 -0
  29. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/backtest.py +0 -0
  30. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/factors.py +0 -0
  31. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/pairs.py +0 -0
  32. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/performance.py +0 -0
  33. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/profile.py +0 -0
  34. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/risk.py +0 -0
  35. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/screen.py +0 -0
  36. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/series_shapes.py +0 -0
  37. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/stress.py +0 -0
  38. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/technical.py +0 -0
  39. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/validation.py +0 -0
  40. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/demo.py +0 -0
  41. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/demo_book.py +0 -0
  42. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/demo_returns.py +0 -0
  43. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/demo_signal.py +0 -0
  44. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/demo_universe.py +0 -0
  45. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/loaders.py +0 -0
  46. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/model.py +0 -0
  47. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/py.typed +0 -0
  48. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/study/__init__.py +0 -0
  49. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/study/schema.py +0 -0
  50. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/sweep/__init__.py +0 -0
  51. {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/sweep/runner.py +0 -0
  52. {alphaengine-0.3.0 → alphaengine-0.4.0}/tests/test_agent.py +0 -0
  53. {alphaengine-0.3.0 → alphaengine-0.4.0}/tests/test_answer.py +0 -0
  54. {alphaengine-0.3.0 → alphaengine-0.4.0}/tests/test_commands.py +0 -0
  55. {alphaengine-0.3.0 → alphaengine-0.4.0}/tests/test_goldens.py +0 -0
  56. {alphaengine-0.3.0 → alphaengine-0.4.0}/tests/test_loaders.py +0 -0
  57. {alphaengine-0.3.0 → alphaengine-0.4.0}/tests/test_screen.py +0 -0
  58. {alphaengine-0.3.0 → alphaengine-0.4.0}/tests/test_signals.py +0 -0
  59. {alphaengine-0.3.0 → alphaengine-0.4.0}/tests/test_sweep.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alphaengine
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Validated research tooling for investment strategies: deflation, overfitting detection, and honest trial counts.
5
5
  Project-URL: Homepage, https://github.com/quantOSC/alphaengine
6
6
  Project-URL: Documentation, https://github.com/quantOSC/alphaengine#readme
@@ -149,6 +149,7 @@ distinction is the whole of the data boundary below.
149
149
  | Command | Does | Where |
150
150
  |---|---|---|
151
151
  | `demo` | run the built-in example offline, with no account and no data | shell + session |
152
+ | `runs [--limit N]` | your own week: what ran, what it decided, what it filed | shell + session |
152
153
  | `workflows` | what the server offers, what each needs, and which reproduce | shell + session |
153
154
  | `key [quantos \| anthropic \| openai]` | enter a credential now, or see which rungs are unlocked | session |
154
155
  | `commands [verb]` | this directory, or one command in full | shell + session |
@@ -209,6 +210,7 @@ distinction is the whole of the data boundary below.
209
210
  | `--label TEXT` | what to call the artifact this run produces |
210
211
  | `--input K=V` | a workflow input; repeatable |
211
212
  | `--quiet` | only the result, no step narration |
213
+ | `--limit N` | how many rows to show (default 25) |
212
214
 
213
215
  ### Examples
214
216
 
@@ -222,6 +224,8 @@ alphaengine overlap
222
224
  alphaengine size
223
225
  alphaengine monitor
224
226
  alphaengine demo
227
+ alphaengine runs
228
+ alphaengine runs --limit 50
225
229
  alphaengine workflows
226
230
  alphaengine commands
227
231
  alphaengine run screen_universe --universe sp500
@@ -108,6 +108,7 @@ distinction is the whole of the data boundary below.
108
108
  | Command | Does | Where |
109
109
  |---|---|---|
110
110
  | `demo` | run the built-in example offline, with no account and no data | shell + session |
111
+ | `runs [--limit N]` | your own week: what ran, what it decided, what it filed | shell + session |
111
112
  | `workflows` | what the server offers, what each needs, and which reproduce | shell + session |
112
113
  | `key [quantos \| anthropic \| openai]` | enter a credential now, or see which rungs are unlocked | session |
113
114
  | `commands [verb]` | this directory, or one command in full | shell + session |
@@ -168,6 +169,7 @@ distinction is the whole of the data boundary below.
168
169
  | `--label TEXT` | what to call the artifact this run produces |
169
170
  | `--input K=V` | a workflow input; repeatable |
170
171
  | `--quiet` | only the result, no step narration |
172
+ | `--limit N` | how many rows to show (default 25) |
171
173
 
172
174
  ### Examples
173
175
 
@@ -181,6 +183,8 @@ alphaengine overlap
181
183
  alphaengine size
182
184
  alphaengine monitor
183
185
  alphaengine demo
186
+ alphaengine runs
187
+ alphaengine runs --limit 50
184
188
  alphaengine workflows
185
189
  alphaengine commands
186
190
  alphaengine run screen_universe --universe sp500
@@ -17,4 +17,16 @@ is what a changed figure costs. The API may still move underneath it.
17
17
  # column, so any grid with a failure read the wrong survivor's column (or
18
18
  # raised). Fixing it changes the figure a 0.2 install would have produced from
19
19
  # the same inputs — which is exactly what the MINOR position costs here.
20
- __version__ = "0.3.0"
20
+ #
21
+ # 0.4.0 IS THE SAME RULE AGAIN, and it is the harder case to spot because it
22
+ # reads as "we added some charts". The wire's figure cap went 64 -> 512 and the
23
+ # per-period cap in `core.signals` went with it, so the SAME INPUTS NOW PRODUCE
24
+ # DIFFERENT OUTPUT: `ic_by_period` returns up to 512 readings where it returned
25
+ # 64, `best_curve` and `drawdown_curve` carry a curve rather than a sketch of
26
+ # one, a grid of up to 512 configurations records its full surface instead of
27
+ # reporting `trials_recorded: false`, and `compute.overlap` emits a rolling
28
+ # correlation and a scatter it did not emit before.
29
+ #
30
+ # Not one of those is a bug fix, and every one changes what a saved study
31
+ # reproduces. That costs the MINOR position while the leading digit is 0.
32
+ __version__ = "0.4.0"
@@ -112,6 +112,34 @@ _ENV_KEY = "QUANTOS_API_KEY"
112
112
  _ENV_URL = "QUANTOS_API_URL"
113
113
  DEFAULT_BASE_URL = "https://alpha-backend-production-51df.up.railway.app"
114
114
 
115
+ #: WHERE A RUN IS READ. The CLI printed a bare run id and left the reader to
116
+ #: work out that `/portal/runs/<id>` exists — and named four screens that had
117
+ #: been redirects for months besides ("Work", "the sleeve page", "your Book",
118
+ #: "accept it in the portal", the last pointing at a surface that does not
119
+ #: exist at all). An id nobody can turn into a URL is a reference to nothing.
120
+ #:
121
+ #: Overridable, and derived from the API host when that has been pointed
122
+ #: somewhere else, so a self-hosted deployment does not send its users to ours.
123
+ DEFAULT_PORTAL_URL = "https://qosai.app"
124
+
125
+
126
+ def portal_url(api_url: str | None = None) -> str:
127
+ """The portal this account's runs live on.
128
+
129
+ `$QUANTOS_PORTAL_URL` wins. Otherwise the public portal — EXCEPT when the
130
+ API has been pointed at localhost, where sending somebody to the hosted
131
+ portal to read a run that exists only on their machine is the config-default
132
+ failure this codebase has already paid for four times: present, wrong, and
133
+ answering every check.
134
+ """
135
+ override = os.environ.get("QUANTOS_PORTAL_URL")
136
+ if override:
137
+ return override.rstrip("/")
138
+ host = (api_url or os.environ.get(_ENV_URL) or DEFAULT_BASE_URL).lower()
139
+ if "localhost" in host or "127.0.0.1" in host:
140
+ return "http://localhost:3000"
141
+ return DEFAULT_PORTAL_URL
142
+
115
143
 
116
144
  # ── output ─────────────────────────────────────────────────────────────────
117
145
  #
@@ -549,6 +577,69 @@ def cmd_workflows(args: argparse.Namespace) -> int:
549
577
  return 0
550
578
 
551
579
 
580
+ def cmd_runs(args: argparse.Namespace) -> int:
581
+ """YOUR OWN WEEK, from where the work happens.
582
+
583
+ The terminal could start a run and could not show you one. "What did I
584
+ already try" was a question you had to open a browser to ask, from the one
585
+ place you are least likely to want to leave — and a quant's week is fifteen
586
+ runs of which most are stops.
587
+
588
+ THE STOPS ARE THE POINT. They are the majority of a week's output and the
589
+ part that evaporates everywhere else, so a stop prints its REASON rather
590
+ than only its token: the sentence is the most useful thing the run produced.
591
+ """
592
+ from .client import Offline, ServerError
593
+
594
+ session, url = _session(args.url, args.key)
595
+ try:
596
+ rows = session.recent_runs(limit=args.limit)
597
+ except Offline:
598
+ _explain_offline(url)
599
+ return 2
600
+ except ServerError as exc:
601
+ refused(exc)
602
+ if is_auth_error(exc) and offer_key():
603
+ say(dim(" Key set. Run that again."))
604
+ return 2
605
+
606
+ if not rows:
607
+ say("")
608
+ say(dim(" Nothing recorded on this account yet."))
609
+ say(dim(" A run files itself as it goes: ") + "alphaengine screen --universe <name>")
610
+ return 0
611
+
612
+ say("")
613
+ for r in rows:
614
+ when = str(r.get("created_at") or "")[:10]
615
+ kind = str(r.get("kind") or "workflow")
616
+ verdict = str(r.get("verdict") or r.get("status") or "")
617
+ mark = (
618
+ yellow("stopped")
619
+ if verdict == "stopped"
620
+ else green("closed")
621
+ if verdict == "completed"
622
+ else dim(verdict or "open")
623
+ )
624
+ line = f" {dim(when)} {bold(kind.ljust(16))} {mark}"
625
+ # THE DENOMINATOR TRAVELS WITH THE COUNT, here as everywhere. A number
626
+ # that was counted and one that was asserted are different claims.
627
+ if r.get("honest_n") is not None:
628
+ src = str(r.get("n_trials_source") or "unknown").replace("_", " ")
629
+ line += dim(f" n={r['honest_n']} {DOT} {src}")
630
+ filed = r.get("signal_file")
631
+ if isinstance(filed, dict) and filed.get("version"):
632
+ line += dim(f" {DOT} filed v{filed['version']}")
633
+ say(line)
634
+ reason = str(r.get("reason") or "").strip()
635
+ if reason:
636
+ for text in _wrap(reason, 68):
637
+ say(" " + dim(text))
638
+ say("")
639
+ say(dim(f" {len(rows)} shown {DOT} open one in the portal to act on it"))
640
+ return 0
641
+
642
+
552
643
  def preflight(catalogue: list[dict[str, Any]], name: str, *, data: Any, backtest_fn: Any) -> str | None:
553
644
  """What is missing before a run starts. `None` when nothing is.
554
645
 
@@ -1028,7 +1119,14 @@ def _propose_sleeve(session: Any, run: Any, thesis: dict[str, Any]) -> None:
1028
1119
 
1029
1120
  say("")
1030
1121
  say(f" {green('Proposed.')} {len(ranked)} names for {bold(title)}")
1031
- say(dim(" Nothing exists yet. Open it in the portal to accept or change it."))
1122
+ # THE PROPOSAL SURFACE WAS RETIRED IN PHASE 8 and this line went on naming
1123
+ # it, so the one instruction printed here pointed at a redirect. Accepting
1124
+ # work happens on the RUN now, which is where its figures, its caveats and
1125
+ # its review pass already are.
1126
+ say(
1127
+ dim(" Nothing exists yet. Accept or decline it on the run: ")
1128
+ + f"{portal_url()}/portal/runs/{getattr(run, 'run_id', '')}"
1129
+ )
1032
1130
  say(dim(f" proposal {filed.get('id')}"))
1033
1131
 
1034
1132
 
@@ -1106,15 +1204,63 @@ def _publish_signals(session: Any, run: Any, *, workspace_id: str | None, label:
1106
1204
  + dim(f"{saved.get('label')} {DOT} {saved.get('asof')}")
1107
1205
  + (dim(f" {DOT} v{version}") if version else "")
1108
1206
  )
1109
- say(dim(" It is on your account, dated and versioned. Tomorrow's run diffs against it."))
1207
+ _say_diff(run)
1208
+
1209
+
1210
+ def _found(before: set[str], run: Any) -> str:
1211
+ """What the step that just ran FOUND, in the reader's words.
1212
+
1213
+ ── THE RULE THAT MAKES THIS SAFE: NARRATION IS RETROSPECTIVE ──────────────
1214
+
1215
+ `protocol.py` is emphatic that the client must never learn the graph — a
1216
+ step names an operation and never a purpose, and "a progress bar is a graph
1217
+ disclosure in a friendly hat". So this may never say what comes NEXT.
1218
+
1219
+ It does not have to. Every word here describes a step that has ALREADY run,
1220
+ and the past is not the graph's future. `executor.execute` returns the
1221
+ figures and `Run.step` records them by op before the round trip, so the
1222
+ client already knows what it just found out, in numbers, as it finds them.
1223
+
1224
+ Only figures this step added, and only ones carrying a LABEL.
1225
+ `_FIGURE_LABELS` is the same vocabulary the closing recap uses — a figure
1226
+ called one thing while it streams and another when it lands is two figures
1227
+ to a reader.
1228
+ """
1229
+ fresh: dict[str, Any] = {}
1230
+ for op, blob in (getattr(run, "figures", {}) or {}).items():
1231
+ if op in before or not isinstance(blob, dict):
1232
+ continue
1233
+ fresh.update(blob)
1234
+
1235
+ parts: list[str] = []
1236
+ # A STATUS OR A VERDICT OUTRANKS A NUMBER. "the book again" is the entire
1237
+ # result of an overlap run, and printing a correlation instead is how the
1238
+ # one conclusion becomes the one thing left unsaid.
1239
+ for key in ("status", "verdict"):
1240
+ word = fresh.get(key)
1241
+ if isinstance(word, str) and word:
1242
+ parts.append(bold(word.replace("_", " ")))
1243
+ break
1244
+ for key, (label, unit) in _FIGURE_LABELS.items():
1245
+ if len(parts) >= 3: # three is a line; more is a table, and one lands below
1246
+ break
1247
+ v = fresh.get(key)
1248
+ if isinstance(v, (int, float)) and not isinstance(v, bool):
1249
+ parts.append(f"{label} {bold(_num(v))}{(' ' + unit) if unit else ''}")
1250
+ return f" {DOT} ".join(parts)
1110
1251
 
1111
1252
 
1112
1253
  def _drive(run: Any, *, quiet: bool = False) -> None:
1113
- """Execute the run, narrating each step.
1254
+ """Execute the run, narrating each step BY WHAT IT FOUND.
1114
1255
 
1115
1256
  Wraps `Run.step` rather than calling `drive()` so there is something to
1116
1257
  watch. The loop itself — what is permitted, in what order, what stops it —
1117
1258
  is still entirely the server's.
1259
+
1260
+ IT USED TO PRINT THE OP NAME AND A DURATION: `server -> compute.screen 1.2s`.
1261
+ That is a progress log. A quant watching it learned which functions fired
1262
+ and never what was being found out — and the process IS the product here, so
1263
+ a run that will not say what it is discovering is showing the wrong half.
1118
1264
  """
1119
1265
  n = 0
1120
1266
  failures: dict[str, int] = {}
@@ -1128,6 +1274,7 @@ def _drive(run: Any, *, quiet: bool = False) -> None:
1128
1274
  for step in batch:
1129
1275
  op = step.get("op", "?")
1130
1276
  before = run.status
1277
+ seen = set(getattr(run, "figures", {}) or {})
1131
1278
  # THE STEP IS WATCHED WHILE IT RUNS, not announced and then silent.
1132
1279
  # `compute.*` executes in-process against the caller's own frames, so
1133
1280
  # this is the one place in the product where a long wait is entirely
@@ -1160,7 +1307,12 @@ def _drive(run: Any, *, quiet: bool = False) -> None:
1160
1307
  }
1161
1308
  return
1162
1309
  elif not quiet:
1163
- say(f" {dim('server ' + ARROW)} {bold(op)} {took}")
1310
+ # WHAT IT FOUND, or the op when it produced nothing nameable —
1311
+ # a step with no labelled figure is a real thing (`record.*`,
1312
+ # `emit.*` seal rather than measure) and printing a blank line
1313
+ # for it would read as a stall.
1314
+ found = _found(seen, run)
1315
+ say(f" {dim(ARROW)} {found or bold(op)} {took}")
1164
1316
 
1165
1317
  if run.status != "open":
1166
1318
  return
@@ -1261,6 +1413,7 @@ def _report(run: Any) -> int:
1261
1413
  if run.status == "closed":
1262
1414
  say(green("Closed.") + " " + dim(str((run.artifact or {}).get("workflow", ""))))
1263
1415
  _render_artifact(run.artifact or {})
1416
+ _say_gaps(run)
1264
1417
  _where_it_lives(run)
1265
1418
  _say_next(run)
1266
1419
  return 0
@@ -1268,6 +1421,10 @@ def _report(run: Any) -> int:
1268
1421
  stop = run.stopped or {}
1269
1422
  say(yellow("Stopped.") + " " + str(stop.get("reason", "")))
1270
1423
  say(dim(" A stop is a result. The run did what it was built to do."))
1424
+ # A STOP GETS ITS GAPS TOO, and needs them more than a close does: the
1425
+ # run refused to answer, so what it could not cover is most of what
1426
+ # there is to know about it.
1427
+ _say_gaps(run)
1271
1428
  _where_it_lives(run)
1272
1429
  return 0
1273
1430
  if run.status == "abandoned":
@@ -1284,6 +1441,96 @@ def _report(run: Any) -> int:
1284
1441
  return 1
1285
1442
 
1286
1443
 
1444
+ def _say_diff(run: Any) -> None:
1445
+ """WHAT MOVED since the last file of the same label.
1446
+
1447
+ Nobody reads a hundred ranked names every morning; they read what changed. A
1448
+ weekly screen's real question is the delta, and until now it was answerable
1449
+ only in a browser — from the one place the person asking it is least likely
1450
+ to want to leave.
1451
+
1452
+ NO PREDECESSOR IS ITS OWN SENTENCE. "First file of this label — nothing to
1453
+ compare against" and "nothing moved" are opposite answers on the one morning
1454
+ somebody is looking hardest, and collapsing them is the failure the
1455
+ `first_file` flag exists to prevent.
1456
+ """
1457
+ try:
1458
+ diff = run.filed_diff()
1459
+ except Exception: # noqa: BLE001 — a delta is worth losing; a result is not
1460
+ return
1461
+ if not diff:
1462
+ return
1463
+
1464
+ if diff.get("first_file"):
1465
+ say(dim(" First file under this label — nothing to compare against yet."))
1466
+ return
1467
+
1468
+ n_in = int(diff.get("n_entered") or 0)
1469
+ n_out = int(diff.get("n_exited") or 0)
1470
+ n_moved = int(diff.get("n_moved") or 0)
1471
+ n_held = len(diff.get("held") or [])
1472
+ if not (n_in or n_out or n_moved):
1473
+ say(dim(f" Nothing moved against {diff.get('compared_with')} {DOT} {n_held} names, same ranking."))
1474
+ return
1475
+
1476
+ parts = []
1477
+ if n_in:
1478
+ parts.append(green(f"{n_in} in"))
1479
+ if n_out:
1480
+ parts.append(red(f"{n_out} out"))
1481
+ if n_moved:
1482
+ parts.append(f"{n_moved} re-ranked")
1483
+ parts.append(dim(f"{n_held} unchanged"))
1484
+ say(f" {dim('vs ' + str(diff.get('compared_with')))} {DOT} " + f" {DOT} ".join(parts))
1485
+
1486
+ # THE NAMES, not just the counts — a count tells you something happened and
1487
+ # a desk still has to open a browser to find out what.
1488
+ for key, colour, word in (("entered", green, "in"), ("exited", red, "out")):
1489
+ names = [str(r.get("ticker")) for r in (diff.get(key) or []) if r.get("ticker")]
1490
+ if names:
1491
+ shown = ", ".join(names[:12])
1492
+ more = f" +{len(names) - 12} more" if len(names) > 12 else ""
1493
+ say(f" {colour(word)} {dim(shown + more)}")
1494
+
1495
+
1496
+ def _say_gaps(run: Any) -> None:
1497
+ """WHAT THIS RUN DOES NOT TELL YOU, printed where it can still be acted on.
1498
+
1499
+ The quant sees the gap at the moment of the run — before delivering, while
1500
+ closing it is still cheap — and the PM sees the SAME derived object on the
1501
+ run's page at the moment of the decision. One derivation, two readings; that
1502
+ is the whole reason it lives on the server rather than in this file.
1503
+
1504
+ THE VOICE IS THE ONE THIS TERMINAL ALREADY USES FOR MEASUREMENT FACTS:
1505
+ quiet, factual, no colour. A gap is not a warning — it is a statement about
1506
+ what was and was not asked, and amber on "nobody has measured these scores
1507
+ yet" would make a normal Tuesday read as an alarm.
1508
+
1509
+ Never fatal. `Run.gaps()` swallows its own transport failures and returns
1510
+ `[]`, so a server that cannot answer costs the block and not the result.
1511
+ """
1512
+ try:
1513
+ gaps = run.gaps()
1514
+ except Exception: # noqa: BLE001 — a recap must never take the run down
1515
+ return
1516
+ if not gaps:
1517
+ return
1518
+
1519
+ say("")
1520
+ say(dim(" What this does not tell you"))
1521
+ for g in gaps:
1522
+ says = str(g.get("says") or "").strip()
1523
+ if not says:
1524
+ continue
1525
+ for i, line in enumerate(_wrap(says, 72)):
1526
+ say((" " if i else " " + dim("·") + " ") + dim(line))
1527
+ # THE REMEDY IS A COMMAND, not a suggestion to think about it. A gap
1528
+ # that cannot name one prints nothing here rather than a vague verb.
1529
+ closes = str(g.get("closes_with") or "")
1530
+ if closes:
1531
+ say(" " + dim("closes with ") + f"alphaengine run {closes}")
1532
+
1533
+
1287
1534
  def _say_next(run: Any) -> None:
1288
1535
  """The next verb, derived from what the run actually produced.
1289
1536
 
@@ -1317,7 +1564,7 @@ def _say_next(run: Any) -> None:
1317
1564
  elif flat.get("deflated_sharpe") is not None:
1318
1565
  line = "find where it breaks: run stress_study --input turnover=<one-way per period>"
1319
1566
  elif flat.get("target_weight") is not None:
1320
- line = "record the fill against your Book in the portal."
1567
+ line = "record the fill on the Portfolio, or take the whole list at once."
1321
1568
  elif flat.get("mean_ic") is not None:
1322
1569
  line = "it carries information; give it a simulator: run validate_study --project <module>"
1323
1570
  elif "usable" in flat:
@@ -1342,7 +1589,7 @@ def _say_next(run: Any) -> None:
1342
1589
  else "the data is clean: run screen_universe"
1343
1590
  )
1344
1591
  elif flat.get("status") == "breached":
1345
- line = "the sleeve page shows the breach against its budget."
1592
+ line = "it crossed a line: the breach is waiting for the desk in the portal."
1346
1593
 
1347
1594
  if line:
1348
1595
  say(dim(f" next {DOT} ") + line)
@@ -1362,11 +1609,10 @@ def _where_it_lives(run: Any) -> None:
1362
1609
  if not run_id:
1363
1610
  return
1364
1611
  say("")
1365
- say(
1366
- dim(f" Recorded {DOT} ")
1367
- + f"{run_id}"
1368
- + dim(f" {DOT} on your account. Open Work in the portal to see it rendered.")
1369
- )
1612
+ # THE URL, NOT THE ID. A reader who has to know that `/portal/runs/<id>`
1613
+ # exists is a reader who does not open it. Most terminals make this
1614
+ # clickable; the ones that do not still let it be copied whole.
1615
+ say(dim(" Recorded ") + f"{portal_url()}/portal/runs/{run_id}")
1370
1616
 
1371
1617
 
1372
1618
  #: How many shortlist rows the terminal prints before saying how many it kept
@@ -2862,6 +3108,12 @@ def cmd_repl(args: argparse.Namespace) -> int:
2862
3108
  if verb == "workflows":
2863
3109
  cmd_workflows(args)
2864
3110
  continue
3111
+ if verb == "runs":
3112
+ # THE SESSION HAS ONE TOO, and it is the same record. A quant
3113
+ # mid-session asking "what have I already tried" should not have to
3114
+ # leave to find out.
3115
+ cmd_runs(argparse.Namespace(url=args.url, key=args.key, limit=25))
3116
+ continue
2865
3117
  if verb == "status":
2866
3118
  say(dim("no run yet") if last is None else f"{last.run_id} {last.status}")
2867
3119
  continue
@@ -3076,6 +3328,8 @@ def build_parser() -> argparse.ArgumentParser:
3076
3328
  sub.add_parser("version", parents=[common], help="print the version")
3077
3329
  sub.add_parser("workflows", parents=[common], help="list what the server offers")
3078
3330
  sub.add_parser("demo", parents=[common], help="run the built-in example, offline")
3331
+ rl = sub.add_parser("runs", parents=[common], help="your own week: what ran, and what it decided")
3332
+ rl.add_argument("--limit", type=int, default=25, help="how many to show (default 25)")
3079
3333
  sub.add_parser("logout", parents=[common], help="remove stored credentials")
3080
3334
  d = sub.add_parser("commands", parents=[common], help="every command, grouped")
3081
3335
  d.add_argument("verb", nargs="?", help="expand one command in full")
@@ -3143,6 +3397,7 @@ def main(argv: list[str] | None = None) -> int:
3143
3397
  handlers: dict[str | None, Callable[[argparse.Namespace], int]] = {
3144
3398
  "version": cmd_version,
3145
3399
  "workflows": cmd_workflows,
3400
+ "runs": cmd_runs,
3146
3401
  "demo": cmd_demo,
3147
3402
  "logout": cmd_logout,
3148
3403
  "commands": cmd_commands,
@@ -61,19 +61,26 @@ Figures = dict[str, Any]
61
61
  Workspace = dict[str, Any]
62
62
  Handler = Callable[[Figures, Workspace], Figures]
63
63
 
64
- # Mirrors the server's guard. Enforced on our side too, because "the data never
65
- # leaves" should not depend on the other end remembering to check.
66
- MAX_FIGURE_LIST = 64
64
+ # Mirrors the server's guard (`harness/vocabulary.MAX_LIST_LEN`). Enforced on
65
+ # our side too, because "the data never leaves" should not depend on the other
66
+ # end remembering to check. THE TWO MUST MOVE TOGETHER: a client cap above the
67
+ # server's turns every long figure into a rejected step, and one below it
68
+ # silently truncates work the server would have accepted.
69
+ MAX_FIGURE_LIST = 512
67
70
 
68
71
  # ── the story figures, bounded by construction ─────────────────────────────
69
- # A run's detail page can only draw what travels, and what travels must be a
70
- # SHAPE, never a seriesthe same bargain profile.py strikes with
71
- # `live_by_period`. So the paths below are bucketed HERE, from data this
72
- # machine already holds, to at-or-under the wire's 64-element bound: a derived
73
- # summary of the caller's own backtest, not the backtest.
74
- CURVE_POINTS = 64 # the best configuration's path, and its drawdown
75
- IC_POINTS = 48 # per-period ICs
76
- COST_POINTS = 24 # cost rungs it is a handful of levels
72
+ # A run's detail page can only draw what travels, and what travels is a DERIVED
73
+ # SUMMARY of the caller's own backtest never their input series.
74
+ #
75
+ # RAISED 2026-08-08 with the wire (64 -> 512). At the old bound these curves
76
+ # arrived pre-bucketed to 64 points, which is a sketch of a curve rather than
77
+ # the curve: a 500-day equity path collapsed to 64 buckets loses every drawdown
78
+ # shorter than eight sessions, and the drawdowns are the part a reader is
79
+ # looking for. The bucketing still happens HERE, on the machine that holds the
80
+ # data, so a longer history is still summarised rather than shipped whole.
81
+ CURVE_POINTS = 512 # the best configuration's path, and its drawdown
82
+ IC_POINTS = 256 # per-period ICs
83
+ COST_POINTS = 64 # cost rungs — still a handful of levels in practice
77
84
 
78
85
 
79
86
  def _bucketed(
@@ -374,11 +381,16 @@ class StepExecutor:
374
381
  # The parameter surface, per trial — the artifact a sweep exists to
375
382
  # produce, and a derived statistic per configuration, so recording it
376
383
  # crosses no data boundary. Sent only when the WHOLE grid fits the
377
- # wire's 64-element cap: a sampled surface would read as the full one,
378
- # which is exactly the misrepresentation the cap exists to prevent.
379
- # Failed trials are omitted so a configuration that did not run renders
380
- # as a hole in the surface, never as a number.
381
- if result.n_trials <= 64:
384
+ # wire's cap: a sampled surface would read as the full one, which is
385
+ # exactly the misrepresentation the cap exists to prevent. Failed trials
386
+ # are omitted so a configuration that did not run renders as a hole in
387
+ # the surface, never as a number.
388
+ #
389
+ # BOUND TO THE CAP RATHER THAN REPEATING IT. This was a literal 64 while
390
+ # the cap was 64, so raising one silently left the other — and the
391
+ # symptom would have been a 300-configuration surface still reporting
392
+ # `trials_recorded: false` for no visible reason.
393
+ if result.n_trials <= MAX_FIGURE_LIST:
382
394
  figures["trials"] = [
383
395
  {**t.params, "sharpe": t.sharpe_annualized} for t in result.trials if t.failed is None
384
396
  ]
@@ -659,7 +671,59 @@ class StepExecutor:
659
671
  "compute.overlap found `book_returns` but no candidate. Put the "
660
672
  "idea's own series under `returns`."
661
673
  )
662
- return dict(overlap_stats(candidate, book))
674
+ out = dict(overlap_stats(candidate, book))
675
+
676
+ # THE STORY FIGURES. A correlation and a beta are two numbers standing
677
+ # in for a relationship, and a desk cannot act on the summary alone:
678
+ #
679
+ # the SCATTER is the joint distribution the correlation summarises,
680
+ # and it is where you see that a 0.2 reading is really a cloud plus
681
+ # four shared crashes;
682
+ #
683
+ # ROLLING CORRELATION is whether the reading is stable. An idea that
684
+ # averages 0.2 to the book but runs at 0.9 in every drawdown is the
685
+ # book again exactly when it matters, and the average says the
686
+ # opposite. This is the figure that changes a sizing decision.
687
+ #
688
+ # Both are derived from the caller's own two series, on this machine,
689
+ # and bounded before anything travels.
690
+ if out.get("correlation") is not None:
691
+ c = np.asarray(candidate, dtype=float)
692
+ b = np.asarray(book, dtype=float)
693
+ depth = int(out.get("n_obs") or min(c.size, b.size))
694
+ c, b = c[-depth:], b[-depth:]
695
+
696
+ stride = max(1, depth // CURVE_POINTS)
697
+ out["overlap_scatter"] = [
698
+ {"x": round(float(b[i]), 6), "y": round(float(c[i]), 6)} for i in range(0, depth, stride)
699
+ ]
700
+
701
+ # A quarter of the history, floored so the window is a measurement
702
+ # rather than a coincidence, and skipped entirely when the series
703
+ # is too short to roll — a rolling reading over 12 points would be
704
+ # noise drawn as a trend.
705
+ window = max(20, depth // 4)
706
+ if depth >= window * 2:
707
+ rolling: list[float] = []
708
+ for end in range(window, depth + 1):
709
+ cw, bw = c[end - window : end], b[end - window : end]
710
+ sc, sb = float(cw.std(ddof=1)), float(bw.std(ddof=1))
711
+ if sc == 0 or sb == 0:
712
+ rolling.append(float("nan"))
713
+ continue
714
+ cov = float(np.mean((cw - cw.mean()) * (bw - bw.mean())))
715
+ rolling.append(cov / (sc * sb) * window / (window - 1))
716
+ clean = [v for v in rolling if v == v]
717
+ if clean:
718
+ out["rolling_correlation"] = [
719
+ {"i": i + window, "v": round(v, 6)}
720
+ for i, v in _bucketed(rolling, CURVE_POINTS, _last)
721
+ if v == v
722
+ ]
723
+ out["rolling_window"] = window
724
+ out["max_rolling_correlation"] = round(max(clean), 6)
725
+ out["min_rolling_correlation"] = round(min(clean), 6)
726
+ return out
663
727
 
664
728
  # ── emit / record ──────────────────────────────────────────────────────
665
729
  #
@@ -133,6 +133,52 @@ class Run:
133
133
  out = self.session._get(f"/api/harness/runs/{self.run_id}/trace")
134
134
  return list(out.get("events") or [])
135
135
 
136
+ def gaps(self) -> list[Figures]:
137
+ """WHAT THIS RUN DOES NOT TELL YOU.
138
+
139
+ Derived server-side by a query over the record — which of the eight
140
+ questions were asked, of what data, and when — and never by a model. A
141
+ gap a model was invited to find is one it can decline to find, and worse
142
+ one it can invent. Each gap names the workflow that closes it, which is
143
+ what makes it a next step rather than an observation.
144
+
145
+ NEVER RAISES. A recap that cannot list its gaps is still worth printing;
146
+ one that takes the result down with it is not. `[]` covers both "none
147
+ found" and "could not ask", so the caller that needs to distinguish them
148
+ has to say so itself.
149
+ """
150
+ try:
151
+ out = self.session._get(f"/api/harness/runs/{self.run_id}/gaps")
152
+ except (Offline, ServerError):
153
+ return []
154
+ return list(out.get("gaps") or [])
155
+
156
+ def filed_diff(self) -> Figures | None:
157
+ """What moved since the last file of the same label, or None.
158
+
159
+ NOBODY READS A HUNDRED RANKED NAMES EVERY MORNING; they read what moved.
160
+ A weekly screen's real question is the delta, and it was answerable only
161
+ in a browser while the person asking it was in a terminal.
162
+
163
+ `first_file` distinguishes NO PREDECESSOR from NOTHING CHANGED, which
164
+ are opposite answers on the one morning somebody is looking hardest —
165
+ so the caller must not collapse them.
166
+
167
+ None means the run filed nothing, or the diff could not be read. Never
168
+ raises: a delta is worth losing, a result is not.
169
+ """
170
+ try:
171
+ filed = self.session._get("/api/me/signals?limit=60")
172
+ except (Offline, ServerError):
173
+ return None
174
+ mine = [f for f in (filed.get("signals") or []) if f.get("source_run_id") == self.run_id]
175
+ if not mine:
176
+ return None
177
+ try:
178
+ return self.session._get(f"/api/me/signals/{mine[0].get('id')}/diff")
179
+ except (Offline, ServerError):
180
+ return None
181
+
136
182
  def step(self, step: Figures) -> Figures:
137
183
  """Execute one permitted step locally and report the figures."""
138
184
  attempt_id = uuid.uuid4().hex
@@ -276,6 +322,21 @@ class Session:
276
322
  """Names and versions. That is all a client is given, and all it needs."""
277
323
  return list(self._get("/api/harness/workflows").get("workflows") or [])
278
324
 
325
+ def recent_runs(self, limit: int = 25) -> list[Figures]:
326
+ """YOUR OWN WEEK: what ran, what it decided, what it filed.
327
+
328
+ The terminal could start work and could not show you any of it, so "what
329
+ did I already try" was a question you had to open a browser to ask —
330
+ from the one place you are least likely to want to leave.
331
+
332
+ Rows carry the verdict, the honest trial count with its provenance, and
333
+ the stop reason where there is one. Mostly stops, which is the point:
334
+ the dead ends are the majority of a week's output and the part that
335
+ evaporates everywhere else.
336
+ """
337
+ out = self._get(f"/api/harness/runs?limit={int(limit)}")
338
+ return list(out.get("runs") or [])
339
+
279
340
  def universes(self) -> list[Figures]:
280
341
  """Universes you registered in the portal: names, and the SYMBOLS in them.
281
342
 
@@ -92,6 +92,7 @@ FLAGS: dict[str, Flag] = {
92
92
  "label": Flag("--label", "TEXT", "what to call the artifact this run produces"),
93
93
  "input": Flag("--input", "K=V", "a workflow input; repeatable"),
94
94
  "quiet": Flag("--quiet", "", "only the result, no step narration"),
95
+ "limit": Flag("--limit", "N", "how many rows to show (default 25)"),
95
96
  }
96
97
 
97
98
 
@@ -192,6 +193,26 @@ COMMANDS: tuple[Command, ...] = _question_commands() + (
192
193
  ),
193
194
  examples=("alphaengine demo",),
194
195
  ),
196
+ Command(
197
+ verb="runs",
198
+ group="start",
199
+ args="[--limit N]",
200
+ scope="both",
201
+ purpose="your own week: what ran, what it decided, what it filed",
202
+ body=(
203
+ "The terminal could START work and could not show you any of it, so "
204
+ '"what did I already try" was a question you had to open a browser to '
205
+ "ask, from the one place you are least likely to want to leave.\n\n"
206
+ "MOSTLY STOPS, AND THAT IS THE POINT. The dead ends are the majority "
207
+ "of a research week and the part that evaporates everywhere else, so a "
208
+ "stopped run prints its REASON here rather than only its token: the "
209
+ "sentence is the most useful thing the run produced.\n\n"
210
+ "Every row carries the honest trial count WITH its provenance, because "
211
+ "a count that was derived from a grid that ran and one that was "
212
+ "asserted are different claims about the same number."
213
+ ),
214
+ examples=("alphaengine runs", "alphaengine runs --limit 50"),
215
+ ),
195
216
  Command(
196
217
  verb="workflows",
197
218
  group="start",
@@ -41,7 +41,14 @@ import numpy as np
41
41
 
42
42
  from .series_shapes import series_values
43
43
 
44
- MAX_PERIODS = 64
44
+ #: The longest per-period sequence a reading reports, and the longest bounded
45
+ #: name list beside it.
46
+ #:
47
+ #: RAISED 64 -> 512 on 2026-08-08 with the wire cap. This one is easy to miss and
48
+ #: silently defeats the others: the core truncates `ic_by_period` HERE, before
49
+ #: `StepExecutor` ever buckets it, so leaving this at 64 would have held IC
50
+ #: through time at 64 points no matter what `IC_POINTS` said.
51
+ MAX_PERIODS = 512
45
52
  _RND = 6
46
53
 
47
54
  DEFAULT_HORIZON = 21
@@ -54,7 +54,12 @@ class ReportError(RuntimeError):
54
54
  # here rather than imported from `client` so that `alphaengine.study` never
55
55
  # pulls the client in — a guarantee `test_client.py` enforces, and one worth
56
56
  # more than not repeating fifteen lines.
57
- MAX_FIGURE_LIST = 64
57
+ #
58
+ # DUPLICATED ON PURPOSE, WHICH MEANS IT CAN DRIFT BY ACCIDENT. Raised with
59
+ # `client.executor.MAX_FIGURE_LIST` on 2026-08-08; `test_client.py` holds the
60
+ # two equal so the deliberate duplication cannot become an accidental
61
+ # disagreement.
62
+ MAX_FIGURE_LIST = 512
58
63
 
59
64
 
60
65
  def _guard(payload: dict[str, Any]) -> dict[str, Any]:
@@ -1322,3 +1322,171 @@ def test_an_unreachable_portal_is_not_an_error_here():
1322
1322
  raise RuntimeError("offline")
1323
1323
 
1324
1324
  assert cli._universe_named_in("screen my sp100 universe", _Down()) is None
1325
+
1326
+
1327
+ # ── the run narrates itself, and never leaks the graph ─────────────────────
1328
+ #
1329
+ # The terminal used to print `server -> compute.screen 1.2s`: which functions
1330
+ # fired, never what was being found out. These hold the replacement to the one
1331
+ # rule that makes it safe — NARRATION IS RETROSPECTIVE. Every line describes a
1332
+ # step that has already run, so the past cannot disclose the graph's future.
1333
+
1334
+
1335
+ class _Narrating:
1336
+ """A run whose figures have just gained one op's worth of results."""
1337
+
1338
+ def __init__(self, figures):
1339
+ self.figures = figures
1340
+
1341
+
1342
+ def test_narration_reports_only_what_the_last_step_added():
1343
+ before = {"compute.profile": {"universe_size": 500}}
1344
+ run = _Narrating({**before, "compute.screen": {"n_evaluated": 431, "n_insufficient": 56}})
1345
+ line = cli._found(set(before), run)
1346
+ assert "431" in line and "56" in line
1347
+ # The earlier step's figure is not restated — it was already narrated.
1348
+ assert "500" not in line
1349
+
1350
+
1351
+ def test_a_verdict_outranks_a_number():
1352
+ """ "the book again" is the entire result of an overlap run; a correlation
1353
+ printed instead is the one conclusion left unsaid."""
1354
+ run = _Narrating({"compute.overlap": {"correlation": 0.91, "verdict": "the_book_again"}})
1355
+ line = cli._found(set(), run)
1356
+ assert line.index("the book again") < line.index("0.91")
1357
+
1358
+
1359
+ def test_a_status_leads_the_line():
1360
+ run = _Narrating({"compute.monitor": {"status": "breached", "n_obs": 250}})
1361
+ assert cli._found(set(), run).startswith(cli.bold("breached"))
1362
+
1363
+
1364
+ def test_narration_never_names_an_op_or_a_stage():
1365
+ """Rule 1 of the protocol: a step names an OPERATION, and the reader of this
1366
+ line must not learn even that. Stage names are the graph's shape."""
1367
+ run = _Narrating({"compute.screen": {"n_evaluated": 431}})
1368
+ line = cli._found(set(), run)
1369
+ assert "compute" not in line and "screen" not in line
1370
+
1371
+
1372
+ def test_a_step_that_produced_no_labelled_figure_narrates_nothing():
1373
+ """`emit.*` and `record.*` seal rather than measure. The caller prints the
1374
+ op in that case — a blank line would read as a stall."""
1375
+ run = _Narrating({"emit.study": {"artifact_id": "abc", "sealed": True}})
1376
+ assert cli._found(set(), run) == ""
1377
+
1378
+
1379
+ def test_narration_is_bounded_to_a_line():
1380
+ run = _Narrating({"compute.all": {k: 1.0 for k in list(cli._FIGURE_LABELS)[:9]}})
1381
+ assert cli._found(set(), run).count(cli.DOT) <= 2 # three parts, two separators
1382
+
1383
+
1384
+ # ── the gaps block ─────────────────────────────────────────────────────────
1385
+
1386
+
1387
+ def test_the_gap_block_prints_the_sentence_and_the_command(capsys):
1388
+ class _R:
1389
+ def gaps(self):
1390
+ return [
1391
+ {
1392
+ "code": "scores_never_measured",
1393
+ "kind": "sequence",
1394
+ "says": "These names are ranked by a score nothing has measured.",
1395
+ "closes_with": "evaluate_signal",
1396
+ }
1397
+ ]
1398
+
1399
+ cli._say_gaps(_R())
1400
+ out = capsys.readouterr().out
1401
+ assert "What this does not tell you" in out
1402
+ assert "nothing has measured" in out
1403
+ # THE REMEDY IS A COMMAND, not a suggestion to think about it.
1404
+ assert "alphaengine run evaluate_signal" in out
1405
+
1406
+
1407
+ def test_a_gap_with_no_remedy_prints_no_command(capsys):
1408
+ class _R:
1409
+ def gaps(self):
1410
+ return [
1411
+ {
1412
+ "code": "engine_moved_on",
1413
+ "kind": "provenance",
1414
+ "says": "Computed on an older engine.",
1415
+ "closes_with": "",
1416
+ }
1417
+ ]
1418
+
1419
+ cli._say_gaps(_R())
1420
+ out = capsys.readouterr().out
1421
+ assert "older engine" in out and "alphaengine run" not in out
1422
+
1423
+
1424
+ def test_no_gaps_prints_no_heading(capsys):
1425
+ class _R:
1426
+ def gaps(self):
1427
+ return []
1428
+
1429
+ cli._say_gaps(_R())
1430
+ assert capsys.readouterr().out == ""
1431
+
1432
+
1433
+ def test_a_recap_never_takes_the_run_down(capsys):
1434
+ """The result is already on screen; losing the gap block costs a block."""
1435
+
1436
+ class _R:
1437
+ def gaps(self):
1438
+ raise RuntimeError("the server is having a day")
1439
+
1440
+ cli._say_gaps(_R())
1441
+ assert capsys.readouterr().out == ""
1442
+
1443
+
1444
+ # ── the portal bridge points at addresses that exist ───────────────────────
1445
+ #
1446
+ # The CLI printed a bare run id and named four screens that had been redirects
1447
+ # for months: "Work", "the sleeve page", "your Book", and — worst — "accept it
1448
+ # in the portal", pointing at a proposal surface that no longer exists at all.
1449
+ # An id nobody can turn into a URL is a reference to nothing.
1450
+
1451
+
1452
+ def test_a_run_is_printed_as_a_url_not_a_bare_id(capsys, monkeypatch):
1453
+ monkeypatch.delenv("QUANTOS_PORTAL_URL", raising=False)
1454
+ monkeypatch.delenv("QUANTOS_API_URL", raising=False)
1455
+
1456
+ class _R:
1457
+ run_id = "run_abc123"
1458
+
1459
+ cli._where_it_lives(_R())
1460
+ out = capsys.readouterr().out
1461
+ assert "https://qosai.app/portal/runs/run_abc123" in out
1462
+
1463
+
1464
+ def test_the_portal_follows_the_api_to_localhost(monkeypatch):
1465
+ """A CONFIG DEFAULT THAT IS PRESENT AND WRONG is worse than one that is
1466
+ missing. Sending somebody to the hosted portal to read a run that exists
1467
+ only on their machine is that failure exactly."""
1468
+ monkeypatch.delenv("QUANTOS_PORTAL_URL", raising=False)
1469
+ monkeypatch.setenv("QUANTOS_API_URL", "http://localhost:8000")
1470
+ assert cli.portal_url() == "http://localhost:3000"
1471
+
1472
+
1473
+ def test_the_portal_url_is_overridable(monkeypatch):
1474
+ """A self-hosted deployment must not send its users to ours."""
1475
+ monkeypatch.setenv("QUANTOS_PORTAL_URL", "https://research.example.com/")
1476
+ assert cli.portal_url() == "https://research.example.com"
1477
+
1478
+
1479
+ def test_no_line_names_a_screen_that_is_a_redirect():
1480
+ """Four names that had been redirects since Phase 8. This asserts the
1481
+ SOURCE rather than one code path, because the next one to go stale will be
1482
+ somewhere none of these tests happens to reach."""
1483
+ import pathlib
1484
+
1485
+ source = pathlib.Path(cli.__file__).read_text(encoding="utf-8")
1486
+ for gone in (
1487
+ "Open Work in the portal",
1488
+ "the sleeve page shows",
1489
+ "against your Book in the portal",
1490
+ "Open it in the portal to accept",
1491
+ ):
1492
+ assert gone not in source, gone
@@ -93,12 +93,16 @@ def test_sweep_records_the_parameter_surface_when_the_grid_fits():
93
93
 
94
94
 
95
95
  def test_an_oversized_grid_records_the_absence_not_a_sample():
96
+ """A sampled surface would read as the full one, which is the whole reason
97
+ the cap exists. Sized off MAX_FIGURE_LIST — at 130 trials against a literal
98
+ this test silently inverted when the cap went to 512 and began asserting
99
+ that an oversized grid IS recorded."""
100
+ fast = list(range(2, 15)) # 13
101
+ slow = list(range(20, 20 + 2 * (MAX_FIGURE_LIST // len(fast) + 2), 2))
96
102
  ex = executor()
97
- out = ex.execute(
98
- "compute.sweep",
99
- {"grid": {"fast": list(range(2, 15)), "slow": list(range(20, 40, 2))}},
100
- )
101
- assert out["n_trials"] == 130
103
+ out = ex.execute("compute.sweep", {"grid": {"fast": fast, "slow": slow}})
104
+
105
+ assert out["n_trials"] == len(fast) * len(slow) > MAX_FIGURE_LIST
102
106
  assert "trials" not in out
103
107
  assert out["trials_recorded"] is False
104
108
 
@@ -143,14 +147,23 @@ def test_a_handler_can_be_supplied_for_ops_only_your_environment_answers():
143
147
  # ── the data does not leave, enforced on this side too ─────────────────────
144
148
  def test_the_executor_refuses_to_send_a_series():
145
149
  """Enforced client side as well as server side. 'The data stays put' should
146
- not depend on the other end remembering to check."""
147
- ex = StepExecutor(data=prices(), handlers={"data.describe": lambda p, w: {"px": [1.0] * 500}})
150
+ not depend on the other end remembering to check.
151
+
152
+ SIZED OFF THE CAP, NOT OFF A LITERAL. These fixtures were 500 and 200 while
153
+ the cap was 64; raising it to 512 made both of them legal payloads, and both
154
+ tests went green while asserting nothing at all. A guard test whose fixture
155
+ does not move with the threshold stops testing the guard on the day the
156
+ threshold moves — which is the one day you want it to fail.
157
+ """
158
+ over = [1.0] * (MAX_FIGURE_LIST + 1)
159
+ ex = StepExecutor(data=prices(), handlers={"data.describe": lambda p, w: {"px": over}})
148
160
  with pytest.raises(ValueError, match="stays on your machine"):
149
161
  ex.execute("data.describe", {})
150
162
 
151
163
 
152
164
  def test_a_series_is_caught_however_it_is_nested():
153
- ex = StepExecutor(data=prices(), handlers={"data.describe": lambda p, w: {"a": {"b": list(range(200))}}})
165
+ nested = list(range(MAX_FIGURE_LIST + 1))
166
+ ex = StepExecutor(data=prices(), handlers={"data.describe": lambda p, w: {"a": {"b": nested}}})
154
167
  with pytest.raises(ValueError, match="series"):
155
168
  ex.execute("data.describe", {})
156
169
 
@@ -709,3 +722,31 @@ def test_cost_ladder_emits_the_curve_only_when_it_was_measured():
709
722
  # ABSENCE, never a flat line at zero.
710
723
  bare = ex.execute("compute.cost_ladder", {})
711
724
  assert "cost_curve" not in bare
725
+
726
+
727
+ def test_the_two_figure_caps_are_equal_even_though_they_are_duplicated():
728
+ """`study/report.py` repeats `MAX_FIGURE_LIST` rather than importing it, so
729
+ that `alphaengine.study` never pulls the client in — a real guarantee, and
730
+ the reason the duplication exists.
731
+
732
+ Deliberate duplication still drifts by accident. Both were 64; the wire cap
733
+ moved to 512 on 2026-08-08 and a report path left behind would have refused
734
+ exactly the curves the change exists to allow, from the one code path that
735
+ runs with no server to correct it.
736
+ """
737
+ from alphaengine.study.report import MAX_FIGURE_LIST as REPORT_CAP
738
+
739
+ assert MAX_FIGURE_LIST == REPORT_CAP
740
+
741
+
742
+ def test_the_core_does_not_truncate_a_sequence_before_the_executor_buckets_it():
743
+ """MAX_PERIODS is upstream of every story figure and silently outranks them.
744
+
745
+ `information_coefficient` truncates `ic_by_period` to MAX_PERIODS before
746
+ `StepExecutor` ever sees it, so a core cap below `IC_POINTS` holds the chart
747
+ at the core's number and nothing in the executor says why.
748
+ """
749
+ from alphaengine.core.signals import MAX_PERIODS
750
+
751
+ assert MAX_PERIODS >= IC_POINTS
752
+ assert MAX_PERIODS <= MAX_FIGURE_LIST
@@ -69,15 +69,20 @@ def test_report_refuses_a_series_before_it_leaves_the_machine():
69
69
  series hidden under any key at all. It is duplicated on the server on
70
70
  purpose: a check that runs only on the client is not a check, and one that
71
71
  runs only on the server tells you too late and without naming the field.
72
+
73
+ SIZED OFF THE CAP. The fixture was 500 elements against a cap of 64; when
74
+ the cap moved to 512 the payload became legal and this test would have gone
75
+ on passing while asserting that the guard does nothing.
72
76
  """
73
77
  import pytest
74
78
 
75
- from alphaengine.study.report import _guard
79
+ from alphaengine.study.report import MAX_FIGURE_LIST, _guard
76
80
 
77
81
  _guard({"performance": {"sharpe": 1.2}}) # figures pass
78
82
 
83
+ over = list(range(MAX_FIGURE_LIST + 1))
79
84
  with pytest.raises(ValueError, match="is a series"):
80
- _guard({"performance": {"totally_not_returns": list(range(500))}})
85
+ _guard({"performance": {"totally_not_returns": over}})
81
86
 
82
87
 
83
88
  def test_report_needs_a_key_and_says_so():
File without changes
File without changes
File without changes
File without changes