hypgraph 0.7.0__tar.gz → 0.7.1__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 (87) hide show
  1. {hypgraph-0.7.0 → hypgraph-0.7.1}/CHANGELOG.md +23 -0
  2. {hypgraph-0.7.0 → hypgraph-0.7.1}/PKG-INFO +1 -1
  3. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/__init__.py +1 -1
  4. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/coord.py +9 -2
  5. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/derive.py +24 -0
  6. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/language.py +3 -1
  7. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/profiles.py +16 -2
  8. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/workflow.py +54 -5
  9. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_measure_questions.py +1 -1
  10. hypgraph-0.7.1/tests/test_opposed_answers.py +99 -0
  11. hypgraph-0.7.1/tests/test_rerunnable.py +75 -0
  12. hypgraph-0.7.1/tests/test_stats_not_backtests.py +68 -0
  13. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_workflows.py +7 -7
  14. {hypgraph-0.7.0 → hypgraph-0.7.1}/.gitignore +0 -0
  15. {hypgraph-0.7.0 → hypgraph-0.7.1}/AGENTS.md +0 -0
  16. {hypgraph-0.7.0 → hypgraph-0.7.1}/CITATION.cff +0 -0
  17. {hypgraph-0.7.0 → hypgraph-0.7.1}/LICENSE +0 -0
  18. {hypgraph-0.7.0 → hypgraph-0.7.1}/NOTICE +0 -0
  19. {hypgraph-0.7.0 → hypgraph-0.7.1}/README.md +0 -0
  20. {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/PLAN.md +0 -0
  21. {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
  22. {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/decisions/ADR-002-governance-profiles.md +0 -0
  23. {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/profile-workflows-plan.md +0 -0
  24. {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/review-2026-10-24.md +0 -0
  25. {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/site-plan.md +0 -0
  26. {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/spec.md +0 -0
  27. {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/ui-plan.md +0 -0
  28. {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/workflows/README.md +0 -0
  29. {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/workflows/knowledge.md +0 -0
  30. {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/workflows/optimization.md +0 -0
  31. {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/workflows/scientific.md +0 -0
  32. {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/workflows/software.md +0 -0
  33. {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/workflows/trading.md +0 -0
  34. {hypgraph-0.7.0 → hypgraph-0.7.1}/pyproject.toml +0 -0
  35. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/__main__.py +0 -0
  36. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/bundle.py +0 -0
  37. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/checks.py +0 -0
  38. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/cli.py +0 -0
  39. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/demos/__init__.py +0 -0
  40. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/demos/broad_street.py +0 -0
  41. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/graph.py +0 -0
  42. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/mcp.py +0 -0
  43. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/plan.py +0 -0
  44. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/policy.py +0 -0
  45. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/refusals.py +0 -0
  46. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/report_check.py +0 -0
  47. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/schema.py +0 -0
  48. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/server.py +0 -0
  49. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/skill/SKILL.md +0 -0
  50. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/stats.py +0 -0
  51. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/store.py +0 -0
  52. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/__init__.py +0 -0
  53. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/atlas.css +0 -0
  54. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/atlas.js +0 -0
  55. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/hypgraph.css +0 -0
  56. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/hypgraph.js +0 -0
  57. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/hypgraph.svg +0 -0
  58. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/index.html +0 -0
  59. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
  60. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/vendor/cose-base.js +0 -0
  61. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
  62. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
  63. {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/vendor/layout-base.js +0 -0
  64. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_bundle_telemetry.py +0 -0
  65. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_cancel_releases_lease.py +0 -0
  66. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_coord.py +0 -0
  67. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_d10.py +0 -0
  68. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_engine.py +0 -0
  69. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_engine_fixes.py +0 -0
  70. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_incremental.py +0 -0
  71. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_log_and_check.py +0 -0
  72. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_order_detail.py +0 -0
  73. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_profiles.py +0 -0
  74. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_profiles_041.py +0 -0
  75. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_questions_not_assertions.py +0 -0
  76. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_refusals.py +0 -0
  77. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_report_answers.py +0 -0
  78. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_restates_scale.py +0 -0
  79. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_server_auth.py +0 -0
  80. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_stats_values.py +0 -0
  81. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_surfaces.py +0 -0
  82. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_telemetry_stream.py +0 -0
  83. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_ui_app.py +0 -0
  84. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_ui_engine.py +0 -0
  85. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_unnamed_inputs.py +0 -0
  86. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_versions_not_figures.py +0 -0
  87. {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_vocabulary_friction.py +0 -0
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.1 — 2026-10-27: a computed answer needs its code, and opposed answers keep a question open
4
+
5
+ - **A computation nobody can re-run doesn't settle a computational question.** The optimization workflow's
6
+ `solved`, trading's `backtested` and the scientific workflow's evidenced stage (`result_exists` under a
7
+ scientific profile) count only results something can re-run: the run that produced them carries its code, or
8
+ it is the instrument's (`kind=lab`). Through the hypswarm CLI, with no tool that executes code, the seats wrote
9
+ "exact DP over capacity" and "brute force over 2^21" as prose; the verifier passed a wrong optimum (757 for
10
+ 656) and the question settled. The gate now says which results report a computation with no code behind it.
11
+ `workflow.rerunnable(snap, result)`.
12
+ - **A question settles only when every live answer to it is checked.** A checked answer beside a live unchecked one
13
+ used to settle the question, and the two may disagree: in a real-model trading run a reviewer re-ran the numbers
14
+ behind "no look-ahead" and checked it while "it is look-ahead" stood unchecked on the same question. The board row
15
+ now says `answered` with the unchecked answers named (`unchecked`), the reviewed gate names them, and completion
16
+ waits. An answer nobody may review (its author is the only reviewer) doesn't block; the row lists it under
17
+ `unreviewable`. `derive.unchecked_answers` / `unreviewable_answers`.
18
+ - **Measuring returns isn't trading them.** Trading's BACKTEST_WITHOUT_COSTS no longer fires on a result that counts
19
+ its returns (`n_returns`, `insample_n_returns`, a value in units of returns, days, bars or trades) or reports a
20
+ statistic of them (an autocorrelation, its t, p, standard error or CI). A return, P&L or Sharpe still needs its
21
+ costs. A real-model run lost eight writes measuring a lag-1 autocorrelation.
22
+ - **A confidence interval that excludes zero isn't an exclusion.** "Excludes zero", "excludes 1" (a ratio's
23
+ null) and "rules out chance" say an effect is there, so they no longer need a power check
24
+ (EXCLUSION_WITHOUT_POWER). "Rules out an effect" and "no effect" still do.
25
+
3
26
  ## 0.7.0 — 2026-10-27: measure questions, and claim checks that know a version from a figure
4
27
 
5
28
  - **Measure questions.** A question may say `asks: "measure"`, and its workflow's `measure_skips` gates then don't
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hypgraph
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: The evidence layer for AI research agents: a hash-chained research record that checks every claim as it is written and derives how well each is known.
5
5
  Project-URL: Homepage, https://hypgraph.com
6
6
  Project-URL: Documentation, https://hypgraph.com/engineers/
@@ -16,7 +16,7 @@ Standing is derived on every read, never stored; every write is an event in a SH
16
16
  their numbers; reviews pin versions; independence is counted, not assumed. See https://hypgraph.com.
17
17
  """
18
18
 
19
- __version__ = "0.7.0"
19
+ __version__ = "0.7.1"
20
20
 
21
21
  from .bundle import verify_bundle # noqa: E402
22
22
  from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
@@ -729,7 +729,13 @@ def question_board(g: "Graph", inv: str) -> list[dict]:
729
729
  checked = [c for c in ans if states[c] == "checked"]
730
730
  contested = [c for c in ans if states[c] == "contested"]
731
731
  o = open_orders.get(q)
732
- if checked:
732
+ pending = D.unchecked_answers(s, q)
733
+ if checked and pending:
734
+ # two live answers, one checked: settled only once every live answer is checked (they may disagree)
735
+ status, by = "answered", (f"{', '.join(_code(s, c) for c in pending[:3])} unchecked beside "
736
+ f"{_code(s, checked[0])} (checked) — review it, or withdraw the wrong answer"
737
+ + (f"; order {o['code']} on it" if o else ""))
738
+ elif checked:
733
739
  status, by = "settled", f"{_code(s, checked[0])} (checked by " + ", ".join(
734
740
  x.split(":")[0] for x in D.state(s, checked[0])["review"]["positive"][:2]) + ")"
735
741
  elif contested:
@@ -741,7 +747,8 @@ def question_board(g: "Graph", inv: str) -> list[dict]:
741
747
  else:
742
748
  status, by = "open", ""
743
749
  rows.append({"id": q, "code": n["code"], "text": _title(s, q), "status": status, "by": by,
744
- "answers": [_code(s, c) for c in ans]})
750
+ "answers": [_code(s, c) for c in ans], "unchecked": [_code(s, c) for c in pending],
751
+ "unreviewable": [_code(s, c) for c in D.unreviewable_answers(s, q)]})
745
752
  return rows
746
753
 
747
754
 
@@ -237,6 +237,30 @@ def _moved_since(s: Snap, rid: str) -> list[str]:
237
237
  return out
238
238
 
239
239
 
240
+ def _open_answers(s: Snap, q: str) -> list[str]:
241
+ return [e["src"] for e in s.inn.get(q, []) if e["rel"] == "answers" and s.nodes[e["src"]]["type"] == "claim"
242
+ and lifecycle(s, e["src"]) == "active" and not fallen(s, e["src"]) and not stale(s, e["src"])
243
+ and (state(s, e["src"]).get("review") or {}).get("state") != "checked"]
244
+
245
+
246
+ def reviewable(s: Snap, nid: str) -> bool:
247
+ """Someone besides its author (and the curators) may review it."""
248
+ return bool(board(s, nid) - set(s.policy.curators) - {s.nodes[nid]["created_by"]})
249
+
250
+
251
+ def unchecked_answers(s: Snap, q: str) -> list[str]:
252
+ """Live, fresh claims answering question ``q`` that no reviewer has checked and one could. A question settles only
253
+ when this is empty: a checked answer beside a live unchecked one is two answers the record hasn't reconciled, and
254
+ they may disagree (studies T1: "no look-ahead" checked while "is look-ahead" stood unchecked on the same
255
+ question). An answer nobody may review (its author is the only reviewer) can't block it; ``unreviewable_answers``
256
+ lists those."""
257
+ return [c for c in _open_answers(s, q) if reviewable(s, c)]
258
+
259
+
260
+ def unreviewable_answers(s: Snap, q: str) -> list[str]:
261
+ return [c for c in _open_answers(s, q) if not reviewable(s, c)]
262
+
263
+
240
264
  def stale(s: Snap, nid: str) -> list[str]:
241
265
  """Why a result (or a claim through its results) no longer speaks for what it measured; [] when fresh. A result
242
266
  is stale when the run that produced it used an artifact that is now superseded or withdrawn, or when its
@@ -30,7 +30,9 @@ EXCLUSION = re.compile(
30
30
  r"\b(no (?:effect|difference|impact|influence|benefit|improvement|association|relationship)|"
31
31
  r"makes? no difference|has no bearing|(?:does|do|did)(?: not|n't) (?:affect|matter|help|work|change|improve)|"
32
32
  r"doesn't (?:affect|matter|help|work|change|improve)|not (?:associated|related|correlated)|ineffective|"
33
- r"useless|rules? out|ruled out|excludes?|unrelated to)\b", re.I)
33
+ r"useless|(?:rules? out|ruled out|excludes?)(?!\s+(?:zero|0(?:\.0+)?|the null|null|chance|a zero|one|"
34
+ r"1(?:\.0+)?)\b)|unrelated to)\b", re.I)
35
+ # "the 95 % CI excludes zero", "rules out chance": leaving out the null value asserts an effect, not its absence
34
36
  HEDGE = re.compile(r"\b(could|might|may|likely|probably|perhaps|possibly|suggests?|consistent with|"
35
37
  r"aligns? with|plausibl\w*)\b", re.I)
36
38
 
@@ -583,7 +583,17 @@ def claim_issues(s: D.Snap, inv: str, body: dict, support: list, pending: list)
583
583
 
584
584
  # a statistic *about* returns (a signal's correlation with tomorrow's return, a hit rate, the volatility) isn't a return
585
585
  # a strategy earned and charges no costs: a look-ahead diagnostic was refused three times as a backtest (studies T1)
586
- _STAT_OF_RETURNS = re.compile(r"corr|agree|match|hit_rate|_ic\b|beta|volatil|std|skew|kurt", re.I)
586
+ _STAT_OF_RETURNS = re.compile(r"corr|agree|match|hit_rate|_ic\b|beta|volatil|std|(?:^|_)sd(?:_|$)|skew|kurt|acf|"
587
+ r"autocov|lag\d|"
588
+ r"t_?stat|p_?val|_se\b|ci_|_ci\b", re.I)
589
+ # a count of returns is not a return earned: n_returns, insample_n_returns, N_returns_in_sample (studies T3)
590
+ _COUNT_KEY = re.compile(r"(?:^|_)(?:n|num|count|nobs|obs)(?:_|$)|_count\b", re.I)
591
+ _COUNT_UNITS = {"returns", "days", "bars", "obs", "observations", "count", "draws", "trades", "rows", "n"}
592
+
593
+
594
+ def _a_count(k: str, v: object) -> bool:
595
+ unit = str(v.get("unit") or "").strip().lower() if isinstance(v, dict) else ""
596
+ return bool(_COUNT_KEY.search(k)) or unit in _COUNT_UNITS
587
597
 
588
598
 
589
599
  def result_issues(s: D.Snap, inv: str, body: dict, pending: list | None = None) -> list[Problem]:
@@ -595,7 +605,9 @@ def result_issues(s: D.Snap, inv: str, body: dict, pending: list | None = None)
595
605
  if "objective_needs_feasibility" in p.validators and "objective" in v and "feasible" not in v:
596
606
  out.append(Problem("OBJECTIVE_WITHOUT_FEASIBILITY", "a result reports an objective value without saying whether "
597
607
  "the solution is feasible", fix="add values.feasible (true/false) and bound or gap"))
598
- earned = [k for k in v if re.search(r"return|sharpe|pnl|cagr|profit", k) and not _STAT_OF_RETURNS.search(k)]
608
+ raw = {str(k).lower(): x for k, x in (body.get("values") or {}).items()}
609
+ earned = [k for k in v if re.search(r"return|sharpe|pnl|cagr|profit", k) and not _STAT_OF_RETURNS.search(k)
610
+ and not _a_count(k, raw.get(k))]
599
611
  if "backtest_needs_costs" in p.validators and earned and not _has(v, r"fee|cost|slippage|commission"):
600
612
  out.append(Problem("BACKTEST_WITHOUT_COSTS", "a backtest result reports returns without the costs it charged",
601
613
  fix="add values for fees and slippage (zero is a value; absence isn't)"))
@@ -630,6 +642,8 @@ def _questions(s: D.Snap, inv: str) -> list[str]:
630
642
 
631
643
 
632
644
  def _settling_claim(s: D.Snap, q: str) -> str:
645
+ if D.unchecked_answers(s, q): # a checked answer beside an unchecked one doesn't settle it
646
+ return ""
633
647
  for e in s.inn.get(q, []):
634
648
  c = e["src"]
635
649
  if e["rel"] == "answers" and s.nodes[c]["type"] == "claim" and _live(s, c) and \
@@ -213,8 +213,46 @@ def _stale_why(sc: Scope) -> str:
213
213
  "now)") if old else ""
214
214
 
215
215
 
216
+ def rerunnable(s: D.Snap, r: str) -> bool:
217
+ """Whether something can re-run the computation behind a result: the run that produced it carries its code, or it
218
+ is the instrument's own (kind=lab). A run with neither is a report of a computation. In a real run with no tool
219
+ that executes code, the seats wrote "exact DP over capacity" and "brute force over 2^21" with no code behind them,
220
+ a wrong optimum (757 for 656) passed its verifier, and the question settled (studies O1 through the CLI)."""
221
+ for e in s.inn.get(r, []):
222
+ run = s.nodes.get(e["src"]) or {}
223
+ if e["rel"] == "produced" and run.get("type") == "run" and _has_code(s, e["src"]):
224
+ return True
225
+ return False
226
+
227
+
228
+ def _has_code(s: D.Snap, run: str) -> bool:
229
+ """The instrument's run, or one that carries what it executed: its source (``config.source``, as hypswarm's
230
+ register_run writes it), its ``code`` list, or an artifact it ``uses`` in the role ``code``."""
231
+ b = s.nodes[run].get("body") or {}
232
+ if b.get("kind") == "lab" or str((b.get("config") or {}).get("source") or "").strip() or b.get("code"):
233
+ return True
234
+ return any(e["rel"] == "uses" and (e.get("data") or {}).get("role") == "code" for e in s.out.get(run, []))
235
+
236
+
237
+ def _computed(sc: Scope, rs: list[str]) -> tuple[list[str], str]:
238
+ """The results among ``rs`` something can re-run, and why the others don't count."""
239
+ ok = [r for r in rs if rerunnable(sc.s, r)]
240
+ bare = [r for r in rs if r not in ok]
241
+ why = (f" ({', '.join(sc.code(r) for r in bare[-3:])} {'reports a computation' if len(bare) == 1 else 'report computations'}"
242
+ " with no code behind it: register the run with the code that computed it, so it can be re-run)") if bare else ""
243
+ return ok, why
244
+
245
+
246
+ def _profile(sc: Scope) -> str:
247
+ return str((sc.s.nodes.get(sc.inv, {}).get("body") or {}).get("profile") or "")
248
+
249
+
216
250
  def g_result_exists(sc: Scope, now: str) -> dict:
217
251
  rs = _current_results(sc)
252
+ if _profile(sc).startswith("scientific"):
253
+ # evidenced is the scientific workflow's computed stage ('worker, instrument'): a result there is a computation
254
+ rs, why = _computed(sc, rs)
255
+ return _g(bool(rs), rs, f"no live result on {sc.code(sc.root)} that can be re-run{why}{_stale_why(sc)}")
218
256
  return _g(bool(rs), rs, f"no live result on {sc.code(sc.root)}{_stale_why(sc)}")
219
257
 
220
258
 
@@ -239,6 +277,13 @@ def g_reviewed(sc: Scope, now: str) -> dict:
239
277
  s = sc.s
240
278
  checked = [c for c in sc.claims if _live(s, c) and _checked_since(sc, c)]
241
279
  fresh = [c for c in checked if not D.stale(s, c)]
280
+ pending = D.unchecked_answers(s, sc.root) if s.nodes[sc.root]["type"] == "question" else []
281
+ if fresh and pending:
282
+ others = ", ".join(sc.code(c) for c in pending[:3])
283
+ return _g(False, [], f"{sc.code(fresh[0])} is checked, but {others} also answer{'s' if len(pending) == 1 else ''} "
284
+ f"{sc.code(sc.root)} and nobody has checked {'it' if len(pending) == 1 else 'them'} — review "
285
+ f"{others}, or withdraw whichever answer is wrong: a question settles when every live answer "
286
+ "to it is checked")
242
287
  if fresh:
243
288
  return _g(True, fresh, "")
244
289
  if checked:
@@ -347,7 +392,8 @@ def _trials(v: dict) -> bool:
347
392
  def g_backtested(sc: Scope, now: str) -> dict:
348
393
  s = sc.s
349
394
  nearest, missing = "", []
350
- for r in _current_results(sc):
395
+ results, bare = _computed(sc, _current_results(sc))
396
+ for r in results:
351
397
  v = _vals(s.nodes[r]["body"])
352
398
  miss = []
353
399
  if not _has(v, r"oos|out_of_sample|holdout|test_"):
@@ -361,8 +407,8 @@ def g_backtested(sc: Scope, now: str) -> dict:
361
407
  if not nearest or len(miss) < len(missing):
362
408
  nearest, missing = r, miss
363
409
  if nearest:
364
- return _g(False, [], f"{sc.code(nearest)} lacks {', '.join(missing)}")
365
- return _g(False, [], f"no backtest result on {sc.code(sc.root)}{_stale_why(sc)}")
410
+ return _g(False, [], f"{sc.code(nearest)} lacks {', '.join(missing)}{bare}")
411
+ return _g(False, [], f"no backtest result on {sc.code(sc.root)}{bare}{_stale_why(sc)}")
366
412
 
367
413
 
368
414
  def _position_missing(body: dict) -> list[str]:
@@ -421,14 +467,15 @@ def g_still_valid(sc: Scope, now: str) -> dict:
421
467
 
422
468
  def g_solved(sc: Scope, now: str) -> dict:
423
469
  s = sc.s
424
- for r in _current_results(sc):
470
+ results, bare = _computed(sc, _current_results(sc))
471
+ for r in results:
425
472
  v = _vals(s.nodes[r]["body"])
426
473
  if "feasible" in v and v["feasible"] in (False, 0, "false", "False", "no"):
427
474
  return _g(True, [r], "")
428
475
  if "objective" in v and v.get("feasible") in _TRUE and ("bound" in v or "gap" in v):
429
476
  return _g(True, [r], "")
430
477
  return _g(False, [], f"no result on {sc.code(sc.root)} with objective, feasible=true and a bound or gap (or "
431
- "feasible=false)" + _stale_why(sc))
478
+ "feasible=false)" + bare + _stale_why(sc))
432
479
 
433
480
 
434
481
  def g_result_reviewed(sc: Scope, now: str) -> dict:
@@ -528,6 +575,8 @@ def stage_of(s: D.Snap, t: str, now: str = "") -> dict:
528
575
  if any(st.gate == "tests_pass_current" for st in stages):
529
576
  settled += [(c, "verified failure: an instrument-recorded negative result, checked") for c in checked
530
577
  if any(_lab_negative(s, r) for r in _claim_results(s, c))]
578
+ if settled and s.nodes[q]["type"] == "question" and D.unchecked_answers(s, q):
579
+ settled = [] # another live answer is unchecked: the record hasn't reconciled them
531
580
  if stage is not None and settled:
532
581
  rv = next((i for i, st in enumerate(stages) if st.gate == "reviewed"), len(stages) - 1)
533
582
  return {**base, "workflow": True, "stage": stages[rv].name, "index": rv + 1, "of": len(stages),
@@ -21,7 +21,7 @@ def _g(profile: str, asks: str | None = "measure"):
21
21
 
22
22
 
23
23
  def _measured(g, inv, q):
24
- run = g.create(author="w", type="run", inv=inv, fields={"kind": "agent", "name": "count", "state": "succeeded"},
24
+ run = g.create(author="w", type="run", inv=inv, fields={"kind": "agent", "name": "count", "state": "succeeded", "config": {"source": "print('count')"}},
25
25
  links=[{"rel": "part_of", "to": q}])["id"]
26
26
  rs = g.create(author="w", type="result", inv=inv, fields={"summary": "A is right on 781 of 1000 items.",
27
27
  "values": {"a_correct": {"k": 781, "n": 1000}}, "outcome": "positive"},
@@ -0,0 +1,99 @@
1
+ """A question settles only when every live answer to it is checked. A checked answer beside a live unchecked one is
2
+ two answers the record hasn't reconciled, and they may disagree: in studies T1 a reviewer re-ran the numbers behind
3
+ "no look-ahead" and checked it, while "it is look-ahead" stood unchecked on the same question, and the question
4
+ counted as settled with the wrong answer."""
5
+ from hypgraph import Graph, Policy, profiles
6
+
7
+ H = "harness"
8
+
9
+
10
+ def _g(profile: str = "", asks: str = "measure"):
11
+ g = Graph(policy=Policy(reviewers={"critic"}, extra_reserved={H}))
12
+ f = {"title": "Opposed answers.", "objective": "See two live answers keep a question open."}
13
+ if profile:
14
+ f["profile"] = profile
15
+ inv = g.create(author=H, type="investigation", fields=f)["id"]
16
+ q = g.create(author=H, type="question", inv=inv, code="Q1",
17
+ fields={"text": "Could the signal be known when a trade on it is placed?", "asks": asks})["id"]
18
+ return g, inv, q
19
+
20
+
21
+ def _answer(g, inv, q, author, statement, corr):
22
+ run = g.create(author=author, type="run", inv=inv, fields={"kind": "agent", "name": "corr", "state": "succeeded"},
23
+ links=[{"rel": "part_of", "to": q}])["id"]
24
+ rs = g.create(author=author, type="result", inv=inv, fields={
25
+ "summary": f"The signal's correlation with the next day's return is {corr}.",
26
+ "values": {"corr_next": {"value": corr}}, "outcome": "positive"},
27
+ links=[{"rel": "produced", "from": run}])["id"]
28
+ return g.create(author=author, type="claim", inv=inv, fields={
29
+ "statement": statement, "scope": "prices.csv, every day.",
30
+ "measurements": [{"name": "corr", "key": "corr_next", "result": rs}]},
31
+ links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs}])["id"]
32
+
33
+
34
+ def _board(g, inv):
35
+ return {r["code"]: r for r in g.question_board(inv)}
36
+
37
+
38
+ def test_a_second_live_answer_keeps_the_question_open_until_it_is_checked():
39
+ g, inv, q = _g()
40
+ no = _answer(g, inv, q, "quant", "There is no look-ahead: the correlation with the next day is 0.34.", 0.34)
41
+ g.review(author="critic", target=no, verdict="checked", method="Re-ran the correlation.", notes="0.34 holds.")
42
+ assert _board(g, inv)["Q1"]["status"] == "settled"
43
+ yes = _answer(g, inv, q, "risk", "It is look-ahead: the signal agrees with the next day's move, 0.71 of days.", 0.71)
44
+ row = _board(g, inv)["Q1"]
45
+ assert row["status"] == "answered" and row["unchecked"] == [g.get(yes)["code"]]
46
+ assert "unchecked beside" in row["by"] and g.get(no)["code"] in row["by"]
47
+ crit = {c["name"]: c for c in profiles.completion(g.snapshot(), inv)["criteria"]}
48
+ assert not crit["questions_settled"]["met"]
49
+ # the reviewer reads the second answer and objects: it falls, and the first answer settles the question again
50
+ g.review(author="critic", target=yes, verdict="objection", method="Read both claims against the file.",
51
+ notes="Contradicts the checked answer; the file's signal column is built from the next close.")
52
+ assert _board(g, inv)["Q1"]["status"] == "settled"
53
+
54
+
55
+ def test_two_checked_answers_settle():
56
+ g, inv, q = _g()
57
+ a = _answer(g, inv, q, "quant", "The correlation with the next day's return is 0.34.", 0.34)
58
+ b = _answer(g, inv, q, "risk", "The signal matches the next day's move on 0.71 of days.", 0.71)
59
+ for c in (a, b):
60
+ g.review(author="critic", target=c, verdict="checked", method="Re-ran it.", notes="The numbers hold as stated.")
61
+ assert _board(g, inv)["Q1"]["status"] == "settled" and _board(g, inv)["Q1"]["unchecked"] == []
62
+
63
+
64
+ def test_the_reviewed_gate_names_the_unchecked_answer():
65
+ g, inv, q = _g("trading@2")
66
+ no = _answer(g, inv, q, "quant", "There is no look-ahead: the correlation with the next day is 0.34.", 0.34)
67
+ g.review(author="critic", target=no, verdict="checked", method="Re-ran the correlation.", notes="0.34 holds.")
68
+ assert g.stage(q)["complete"]
69
+ yes = _answer(g, inv, q, "risk", "It is look-ahead: the signal agrees with the next day's move, 0.71 of days.", 0.71)
70
+ st = g.stage(q)
71
+ gate = next(gt for gt in st["gates"] if gt["gate"] == "reviewed")
72
+ assert not st["complete"] and not gate["met"]
73
+ assert g.get(yes)["code"] in gate["why"] and "every live answer" in gate["why"]
74
+
75
+
76
+ def test_a_checked_inconclusive_answer_does_not_end_the_question_beside_an_unchecked_finding():
77
+ g, inv, q = _g("trading@2", asks="")
78
+ run = g.create(author="quant", type="run", inv=inv, fields={"kind": "agent", "name": "read", "state": "succeeded"},
79
+ links=[{"rel": "part_of", "to": q}])["id"]
80
+ rs = g.create(author="quant", type="result", inv=inv, fields={
81
+ "summary": "No column in prices.csv says when the signal was published.", "outcome": "inconclusive"},
82
+ links=[{"rel": "produced", "from": run}])["id"]
83
+ inc = g.create(author="quant", type="claim", inv=inv, fields={
84
+ "statement": "Whether the signal is known in time can't be told from these files.",
85
+ "scope": "prices.csv as given.", "kind": "inconclusive"},
86
+ links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs}])["id"]
87
+ g.review(author="critic", target=inc, verdict="checked", method="Read the files.", notes="Nothing dates the signal.")
88
+ assert g.stage(q)["complete"]
89
+ _answer(g, inv, q, "risk", "It is look-ahead: the signal agrees with the next day's move, 0.71 of days.", 0.71)
90
+ assert not g.stage(q)["complete"]
91
+
92
+
93
+ def test_an_answer_nobody_may_review_does_not_block():
94
+ g, inv, q = _g()
95
+ mine = _answer(g, inv, q, "critic", "The correlation with the next day's return is 0.34.", 0.34) # the only reviewer
96
+ theirs = _answer(g, inv, q, "quant", "The signal matches the next day's move on 0.71 of days.", 0.71)
97
+ g.review(author="critic", target=theirs, verdict="checked", method="Re-ran it.", notes="The numbers hold as stated.")
98
+ row = _board(g, inv)["Q1"]
99
+ assert row["status"] == "settled" and row["unreviewable"] == [g.get(mine)["code"]] and row["unchecked"] == []
@@ -0,0 +1,75 @@
1
+ """A computational gate counts only a result something can re-run: the run that produced it carries its code, or it is
2
+ the instrument's (kind=lab). Through the CLI, with no tool that executes code, the seats of an optimization run
3
+ wrote "exact DP over capacity" and "brute force over 2^21" as prose, the verifier passed a wrong optimum (757 for
4
+ 656) and the question settled. solved (optimization), backtested (trading) and the scientific workflow's evidenced
5
+ stage now want the code."""
6
+ import pytest
7
+
8
+ from hypgraph import Graph, Policy
9
+
10
+ H = "harness"
11
+ SHA = "a" * 64
12
+
13
+
14
+ def _g(profile: str, reviewers=("verifier",)):
15
+ g = Graph(policy=Policy(reviewers=set(reviewers), extra_reserved={H}))
16
+ inv = g.create(author=H, type="investigation", fields={"title": "Load the van.", "profile": profile,
17
+ "objective": "Find the best selection under the weight limit."})["id"]
18
+ q = g.create(author=H, type="question", inv=inv, code="Q1",
19
+ fields={"text": "What is the highest total value that fits in the van?"})["id"]
20
+ return g, inv, q
21
+
22
+
23
+ def _gate(g, q, name):
24
+ return next(x for x in g.stage(q)["gates"] if x["gate"] == name)
25
+
26
+
27
+ def _solve(g, inv, q, run_fields, author="solver"):
28
+ inst = g.search("", "artifact", inv, 5)
29
+ inst = inst[0]["id"] if inst else g.create(author="lead", type="artifact", inv=inv, fields={
30
+ "sha256": SHA, "path": "items.csv", "kind": "instance"}, links=[{"rel": "part_of", "to": q}])["id"]
31
+ run = g.create(author=author, type="run", inv=inv, fields={"name": "dp", "state": "succeeded", **run_fields},
32
+ links=[{"rel": "uses", "to": inst}])["id"]
33
+ rs = g.create(author=author, type="result", inv=inv, fields={"summary": "Exact DP over capacity: the optimum.",
34
+ "values": {"objective": {"value": 757}, "feasible": {"value": True}, "bound": {"value": 757}},
35
+ "outcome": "positive"}, links=[{"rel": "produced", "from": run}])["id"]
36
+ g.create(author=author, type="claim", inv=inv, fields={"statement": "The highest total value is 757 (bound 757).",
37
+ "scope": "items.csv at a 212 kg limit."}, links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs}])
38
+ return rs
39
+
40
+
41
+ def test_a_solve_with_no_code_behind_it_is_not_solved():
42
+ g, inv, q = _g("optimization@2")
43
+ rs = _solve(g, inv, q, {"kind": "agent"})
44
+ gate = _gate(g, q, "solved")
45
+ assert not gate["met"] and "no code behind it" in gate["why"] and g.get(rs)["code"] in gate["why"]
46
+
47
+
48
+ @pytest.mark.parametrize("fields", [{"kind": "agent", "config": {"source": "import csv\nprint('objective', 656)"}},
49
+ {"kind": "agent", "code": ["solve.py"]}, {"kind": "lab"}])
50
+ def test_a_solve_with_its_code_or_by_the_instrument_is_solved(fields):
51
+ g, inv, q = _g("optimization@2")
52
+ _solve(g, inv, q, fields, author="lab" if fields["kind"] == "lab" else "solver")
53
+ assert _gate(g, q, "solved")["met"]
54
+
55
+
56
+ def test_a_backtest_with_no_code_is_not_backtested():
57
+ g, inv, q = _g("trading@2", reviewers=("risk",))
58
+ run = g.create(author="quant", type="run", inv=inv, fields={"kind": "agent", "name": "bt", "state": "succeeded"})["id"]
59
+ g.create(author="quant", type="result", inv=inv, fields={"summary": "Backtest out of sample after costs.",
60
+ "values": {"oos_return": {"value": -0.18}, "fees": {"value": 0.001}, "trials": {"value": 20}},
61
+ "outcome": "negative"}, links=[{"rel": "produced", "from": run}, {"rel": "part_of", "to": q}])
62
+ gate = _gate(g, q, "backtested")
63
+ assert not gate["met"] and "no code behind it" in gate["why"]
64
+
65
+
66
+ def test_a_scientific_result_needs_its_code_but_a_sourced_one_does_not():
67
+ g, inv, q = _g("scientific@2", reviewers=("critic",))
68
+ run = g.create(author="w", type="run", inv=inv, fields={"kind": "agent", "name": "count", "state": "succeeded"},
69
+ links=[{"rel": "part_of", "to": q}])["id"]
70
+ g.create(author="w", type="result", inv=inv, fields={"summary": "A is right on 781 of 1000 items.",
71
+ "values": {"a_correct": {"k": 781, "n": 1000}}, "outcome": "positive"}, links=[{"rel": "produced", "from": run}])
72
+ assert not _gate(g, q, "result_exists")["met"]
73
+ from hypgraph import workflow as W
74
+ assert W.GATES["result_exists"] is W.g_result_exists # the knowledge workflow doesn't use the gate
75
+ assert "result_exists" not in {st.gate for st in __import__("hypgraph").profiles.KNOWLEDGE2.workflow.stages}
@@ -0,0 +1,68 @@
1
+ """Measuring returns isn't trading them, and a confidence interval that leaves out zero isn't an exclusion. A real-model
2
+ trading run (studies T3, a mean-reversion edge) lost eight writes to BACKTEST_WITHOUT_COSTS on a lag-1
3
+ autocorrelation, because the result also counted its returns (`n_returns`), and three claims to
4
+ EXCLUSION_WITHOUT_POWER for "the bootstrap 95% CI excludes zero" — which says the effect is there."""
5
+ import pytest
6
+
7
+ from hypgraph import Graph, GraphError, Policy
8
+ from hypgraph import language as L
9
+
10
+ H = "harness"
11
+
12
+
13
+ def _trading():
14
+ g = Graph(policy=Policy(reviewers={"critic"}, extra_reserved={H}))
15
+ inv = g.create(author=H, type="investigation", fields={"title": "Reversal.", "profile": "trading@1",
16
+ "objective": "Measure the reversal before backtesting it."})["id"]
17
+ q = g.create(author=H, type="question", inv=inv, code="Q1",
18
+ fields={"text": "Is the lag-1 autocorrelation of daily returns distinguishable from zero?"})["id"]
19
+ return g, inv, q
20
+
21
+
22
+ def _result(g, inv, values, summary="Lag-1 autocorrelation of daily returns, in sample."):
23
+ run = g.create(author="quant", type="run", inv=inv, fields={"kind": "agent", "name": "acf", "state": "succeeded"})
24
+ return g.create(author="quant", type="result", inv=inv, fields={"summary": summary, "values": values,
25
+ "outcome": "positive"}, links=[{"rel": "produced", "from": run["id"]}])["id"]
26
+
27
+
28
+ @pytest.mark.parametrize("values", [
29
+ {"acf1_insample": {"value": -0.09995, "unit": "correlation"}, "insample_n_returns": {"value": 1499, "unit": "returns"}},
30
+ {"autocorr_lag1": {"value": -0.1008, "unit": "correlation"}, "n_return_obs_in": {"value": 1500, "unit": "returns"}},
31
+ {"acf1": {"value": -0.100, "unit": "acf"}, "N_returns_in_sample": {"value": 1500, "unit": "returns"},
32
+ "return_t_stat": {"value": -3.87}, "return_p_value": {"value": 0.0001}},
33
+ # the T3 re-run's own body: a standard deviation of returns beside the autocorrelation
34
+ {"in_sample_autocorr_lag1": {"value": -0.1, "unit": "correlation"}, "in_sample_sd_of_daily_return": {"value": 0.0101,
35
+ "unit": "return"}, "n_pairs": {"value": 1498, "unit": "pairs"}, "t_stat": {"value": -3.87, "unit": "t"}},
36
+ ])
37
+ def test_a_statistic_and_a_count_of_returns_need_no_costs(values):
38
+ g, inv, _q = _trading()
39
+ _result(g, inv, values)
40
+
41
+
42
+ def test_a_return_earned_still_needs_costs():
43
+ g, inv, _q = _trading()
44
+ with pytest.raises(GraphError, match="BACKTEST_WITHOUT_COSTS"):
45
+ _result(g, inv, {"oos_return": {"value": 0.756}, "n_trades": {"value": 795, "unit": "trades"}},
46
+ summary="Out-of-sample return of the reversal rule.")
47
+
48
+
49
+ @pytest.mark.parametrize("text", ["the bootstrap 95% CI excludes zero", "the interval excludes 0.0",
50
+ "the odds ratio's CI excludes 1", "a shuffle test rules out chance"])
51
+ def test_leaving_out_the_null_is_not_an_exclusion(text):
52
+ assert L.asserted(L.EXCLUSION, text) == []
53
+
54
+
55
+ @pytest.mark.parametrize("text", ["the trial rules out an effect", "the data exclude a benefit", "there is no effect"])
56
+ def test_real_exclusions_still_count(text):
57
+ assert L.asserted(L.EXCLUSION, text)
58
+
59
+
60
+ def test_a_claim_that_the_interval_excludes_zero_is_written():
61
+ g, inv, q = _trading()
62
+ rs = _result(g, inv, {"acf1": {"value": -0.100}, "boot_ci_lo": {"value": -0.144}, "boot_ci_hi": {"value": -0.048}})
63
+ g.create(author="quant", type="claim", inv=inv, fields={
64
+ "statement": "The lag-1 autocorrelation is -0.100 and its bootstrap 95% CI [-0.144, -0.048] excludes zero.",
65
+ "scope": "bars.csv, days 0-1499.",
66
+ "measurements": [{"name": "acf1", "key": "acf1", "result": rs}, {"name": "lo", "key": "boot_ci_lo", "result": rs},
67
+ {"name": "hi", "key": "boot_ci_hi", "result": rs}]},
68
+ links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs}])
@@ -89,7 +89,7 @@ def test_scientific_stage_is_read_off_the_record():
89
89
  "success_criteria": "every fixture parses"},
90
90
  links=[{"rel": "tests", "to": h, "data": {"expected": "all 9 fixtures parse"}}])["id"]
91
91
  assert _stage(g, q) == ("run", 4, False)
92
- run = g.create(author="w", type="run", inv=inv, fields={"kind": "agent", "name": "parse", "state": "succeeded"},
92
+ run = g.create(author="w", type="run", inv=inv, fields={"kind": "agent", "name": "parse", "state": "succeeded", "config": {"source": "print('parse')"}},
93
93
  links=[{"rel": "executes", "to": x}])["id"]
94
94
  assert _stage(g, q) == ("evidenced", 5, False)
95
95
  rs = g.create(author="w", type="result", inv=inv, fields={"summary": "Parsed 9 of 9 trailing-comma fixtures.",
@@ -192,7 +192,7 @@ def test_uses_is_at_risk_only_under_a_workflow():
192
192
  for pid, expect in (("scientific@1", False), ("scientific@2", True)):
193
193
  g, inv, q = _g(pid)
194
194
  data = g.create(author="w", type="artifact", inv=inv, fields={"sha256": SHA_C, "path": "data.csv", "kind": "data"})["id"]
195
- run = g.create(author="w", type="run", inv=inv, fields={"kind": "agent", "name": "fit", "state": "succeeded"},
195
+ run = g.create(author="w", type="run", inv=inv, fields={"kind": "agent", "name": "fit", "state": "succeeded", "config": {"source": "print('fit')"}},
196
196
  links=[{"rel": "uses", "to": data}])["id"]
197
197
  rs = g.create(author="w", type="result", inv=inv, fields={"summary": "Fitted the model on the data file.",
198
198
  "values": {"rmse": {"value": 0.41}}, "outcome": "positive"}, links=[{"rel": "produced", "from": run}])["id"]
@@ -205,7 +205,7 @@ def test_uses_is_at_risk_only_under_a_workflow():
205
205
  # ── trading ────────────────────────────────────────────────────────
206
206
 
207
207
  def _bt(g, inv, author, values, summary="Backtested the thesis out of sample.", q="", x=""):
208
- r = g.create(author=author, type="run", inv=inv, fields={"kind": "agent", "name": "backtest", "state": "succeeded"},
208
+ r = g.create(author=author, type="run", inv=inv, fields={"kind": "agent", "name": "backtest", "state": "succeeded", "config": {"source": "print('backtest')"}},
209
209
  links=[{"rel": "executes", "to": x}] if x else None)["id"]
210
210
  links = [{"rel": "produced", "from": r}] + ([{"rel": "part_of", "to": q}] if q else [])
211
211
  return g.create(author=author, type="result", inv=inv, fields={"summary": summary, "values": values, "outcome": "positive"},
@@ -260,7 +260,7 @@ def test_trading_trials_positions_and_no_live_orders():
260
260
 
261
261
  def test_valid_until_makes_a_result_stale():
262
262
  g, inv, _q = _g("trading@2")
263
- r = g.create(author="quant", type="run", inv=inv, fields={"kind": "agent", "name": "backtest", "state": "succeeded"})["id"]
263
+ r = g.create(author="quant", type="run", inv=inv, fields={"kind": "agent", "name": "backtest", "state": "succeeded", "config": {"source": "print('backtest')"}})["id"]
264
264
  rs = g.create(author="quant", type="result", inv=inv, fields={"summary": "Backtested the thesis out of sample.",
265
265
  "values": {"oos_return": {"value": 0.08}, "fees": {"value": 0.002}}, "outcome": "positive",
266
266
  "valid_until": "2020-06-30"}, links=[{"rel": "produced", "from": r}])["id"]
@@ -311,7 +311,7 @@ def test_optimization_instance_verifier_and_changed_instance():
311
311
  inst = g.create(author="lead", type="artifact", inv=inv, fields={"sha256": SHA_A, "path": "instances/40-site.json", "kind": "instance"},
312
312
  links=[{"rel": "part_of", "to": q}])["id"]
313
313
  assert _stage(g, q) == ("modelled", 2, False)
314
- run = g.create(author="solver", type="run", inv=inv, fields={"kind": "agent", "name": "cbc", "state": "succeeded"},
314
+ run = g.create(author="solver", type="run", inv=inv, fields={"kind": "agent", "name": "cbc", "state": "succeeded", "config": {"source": "print('cbc')"}},
315
315
  links=[{"rel": "uses", "to": inst}])["id"]
316
316
  rs = g.create(author="solver", type="result", inv=inv, fields={"summary": "Solved the model to a 0.0 gap.",
317
317
  "values": {"objective": {"value": 412.0}, "feasible": {"value": True}, "gap": {"value": 0.0}}, "outcome": "positive"},
@@ -508,7 +508,7 @@ def test_an_edit_elsewhere_on_the_page_keeps_the_source_fresh():
508
508
  def test_a_decision_on_stale_evidence_and_a_check_of_a_stale_solve_do_not_count():
509
509
  g, inv, q = _g("trading@2", reviewers=("risk",))
510
510
  data = g.create(author="quant", type="artifact", inv=inv, fields={"sha256": SHA_A, "path": "prices.csv"})["id"]
511
- run_ = g.create(author="quant", type="run", inv=inv, fields={"kind": "agent", "name": "backtest", "state": "succeeded"},
511
+ run_ = g.create(author="quant", type="run", inv=inv, fields={"kind": "agent", "name": "backtest", "state": "succeeded", "config": {"source": "print('backtest')"}},
512
512
  links=[{"rel": "uses", "to": data}])["id"]
513
513
  rs = g.create(author="quant", type="result", inv=inv, fields={"summary": "Backtested the thesis out of sample.",
514
514
  "values": {"oos_return": {"value": 0.08}, "fees": {"value": 0.002}, "trials": {"value": 1}}, "outcome": "positive"},
@@ -526,7 +526,7 @@ def test_a_decision_on_stale_evidence_and_a_check_of_a_stale_solve_do_not_count(
526
526
  g2, inv2, q2 = _g("optimization@2", reviewers=("verifier",))
527
527
  inst = g2.create(author="lead", type="artifact", inv=inv2, fields={"sha256": SHA_A, "path": "instance.csv", "kind": "instance"},
528
528
  links=[{"rel": "part_of", "to": q2}])["id"]
529
- r2 = g2.create(author="solver", type="run", inv=inv2, fields={"kind": "agent", "name": "solve", "state": "succeeded"},
529
+ r2 = g2.create(author="solver", type="run", inv=inv2, fields={"kind": "agent", "name": "solve", "state": "succeeded", "config": {"source": "print('solve')"}},
530
530
  links=[{"rel": "uses", "to": inst}])["id"]
531
531
  s2 = g2.create(author="solver", type="result", inv=inv2, fields={"summary": "Enumerated the instance.",
532
532
  "values": {"objective": {"value": 40}, "feasible": {"value": True}, "gap": {"value": 0.0}}, "outcome": "positive"},
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes