hypgraph 0.5.0__tar.gz → 0.5.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.
- {hypgraph-0.5.0 → hypgraph-0.5.1}/CHANGELOG.md +21 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/PKG-INFO +1 -1
- {hypgraph-0.5.0 → hypgraph-0.5.1}/docs/profile-workflows-plan.md +5 -1
- {hypgraph-0.5.0 → hypgraph-0.5.1}/docs/workflows/optimization.md +2 -1
- {hypgraph-0.5.0 → hypgraph-0.5.1}/docs/workflows/software.md +5 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/__init__.py +1 -1
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/graph.py +45 -1
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/plan.py +2 -1
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/profiles.py +1 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/workflow.py +91 -11
- {hypgraph-0.5.0 → hypgraph-0.5.1}/tests/test_workflows.py +90 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/.gitignore +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/AGENTS.md +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/CITATION.cff +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/LICENSE +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/NOTICE +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/README.md +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/docs/PLAN.md +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/docs/decisions/ADR-002-governance-profiles.md +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/docs/review-2026-10-24.md +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/docs/spec.md +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/docs/workflows/README.md +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/docs/workflows/knowledge.md +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/docs/workflows/scientific.md +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/docs/workflows/trading.md +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/pyproject.toml +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/__main__.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/bundle.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/checks.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/cli.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/coord.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/demos/__init__.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/demos/broad_street.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/derive.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/language.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/mcp.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/policy.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/refusals.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/report_check.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/schema.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/server.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/skill/SKILL.md +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/stats.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/store.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/ui/__init__.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/ui/atlas.css +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/ui/atlas.js +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/ui/hypgraph.svg +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/ui/index.html +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/ui/vendor/cose-base.js +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/src/hypgraph/ui/vendor/layout-base.js +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/studies/harmony/tests/test_suite.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/tests/test_cancel_releases_lease.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/tests/test_coord.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/tests/test_d10.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/tests/test_engine.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/tests/test_engine_fixes.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/tests/test_incremental.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/tests/test_log_and_check.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/tests/test_order_detail.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/tests/test_profiles.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/tests/test_profiles_041.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/tests/test_questions_not_assertions.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/tests/test_refusals.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/tests/test_server_auth.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/tests/test_stats_values.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/tests/test_surfaces.py +0 -0
- {hypgraph-0.5.0 → hypgraph-0.5.1}/tests/test_vocabulary_friction.py +0 -0
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.1 — 2026-10-26
|
|
4
|
+
|
|
5
|
+
What wiring HypSwarm's scheduler to the stages (P2) found the workflows had no honest ending or no right answer for:
|
|
6
|
+
- **Declining a thesis is a decision.** A decision `kind=no_position` based on the claim meets `positioned` and
|
|
7
|
+
`position_sized` ("1 declined"); only `kind=position` needs size, stop, review date and `paper=true`.
|
|
8
|
+
- **An honest "it can't be answered" settles a question whatever its stage.** A checked claim of kind
|
|
9
|
+
`inconclusive` or `exclusion` that answers the question (no source says it; too few trades; infeasible) completes
|
|
10
|
+
it; `stage()` reports `settled_by` and `settled_how`. The claim still needs its evidence — the search's own
|
|
11
|
+
result ("read 3 of 3 pages; 0 mention it") is what it derives from.
|
|
12
|
+
- **A verified failure settles a software requirement.** A checked claim resting on an instrument-recorded
|
|
13
|
+
*negative* result ("1 of 2 checks pass", re-run by the instrument) completes the question; `tests_pass_current`
|
|
14
|
+
stays unmet and `requirements_verified_current` says so.
|
|
15
|
+
- **The same bytes, registered again, are one node that learns.** A second `create` of an artifact with the same
|
|
16
|
+
sha adds the links given now (`part_of` a question, `supersedes`) and, when the node was untyped, its `kind` /
|
|
17
|
+
`meta` / `path` as a new version. Before, the task's own file silently swallowed the diff registered on it.
|
|
18
|
+
- **A decision that settles a work order is the harness's, not the question's** (`decided` ignores it).
|
|
19
|
+
- **optimization gains a `checked` stage** (the verifier checks the claim after the result): a question settles on a
|
|
20
|
+
checked claim, so the workflow had to ask for one.
|
|
21
|
+
- A review order's target predates the order; `reviewed` / `result_reviewed` now ask that the *review* be new.
|
|
22
|
+
- `report()` names experiments by aim and decisions by choice.
|
|
23
|
+
|
|
3
24
|
## 0.5.0 — 2026-10-26: profiles carry their discipline's workflow
|
|
4
25
|
|
|
5
26
|
Each discipline's method, as the record enforces it — one page each in `docs/workflows/`, taken from the method
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hypgraph
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.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/
|
|
@@ -91,7 +91,11 @@ needs a migration story. P0 is a day and decides whether P1 is right. P3 is the
|
|
|
91
91
|
`tests/test_workflows.py`; CHANGELOG). The schema change turned out additive (fields and relation endpoints), so
|
|
92
92
|
no migration: `@1` records derive exactly as before, the `@2` profiles opt in. Left for later phases, each named in
|
|
93
93
|
its spec: per-workflow evidence rank (documented, not derived), the knowledge re-fetch check, CI as the software
|
|
94
|
-
instrument, mechanical feasibility recomputation (P3). P2
|
|
94
|
+
instrument, mechanical feasibility recomputation (P3). **P2 built in HypSwarm 0.1.0a4** (`scheduler.py`: stage
|
|
95
|
+
orders, review on the node the previous gate produced, the record's word on done; the examples' scripted models walk
|
|
96
|
+
the stages; the discovery stub run through its stages). P2 fed back into hypgraph 0.5.1: honest endings the stages
|
|
97
|
+
lacked (a declined thesis, an inconclusive answer, a verified failure), artifacts that learn their kind on
|
|
98
|
+
re-registration, optimization's `checked` stage. P3 is next.
|
|
95
99
|
|
|
96
100
|
## 4. What stays as it is, on purpose
|
|
97
101
|
|
|
@@ -20,7 +20,8 @@ model is a hypothesis about the problem and can be wrong independently of the so
|
|
|
20
20
|
| 4 | solved | run `executes` the experiment, `uses` the instance; result with `objective`, `feasible`, `bound`, `gap`, `runtime_s`, `solution` artifact | `feasible_bounded` (today) | solver, instrument |
|
|
21
21
|
| 5 | verified | review by the verifier: the solution satisfies the constraints of the **instance artifact** (recomputed, not trusted) | `reviewed` with method naming the check | verifier |
|
|
22
22
|
| 6 | claimed | claim "best found, gap g" or "optimal" with the bound; `OPTIMAL_WITHOUT_BOUND` (today) | `claim_made` | solver |
|
|
23
|
-
| 7 |
|
|
23
|
+
| 7 | checked | the verifier checks the claim's wording against the result (a question settles on a checked claim) | `reviewed` | verifier |
|
|
24
|
+
| 8 | recommended | decision `based_on` the claim: the incumbent to use, with the sensitivity note | `decided` **new** (optional) | lead |
|
|
24
25
|
|
|
25
26
|
Infeasibility is a settled outcome: a result with `feasible: false` and a claim of `kind=exclusion` ("no feasible
|
|
26
27
|
assignment exists: total capacity 7 < 8 customers") settles the question. Today that path trips
|
|
@@ -76,3 +76,8 @@ superseded heads, listed as superseded); open requirements.
|
|
|
76
76
|
reads, so a CI reproducer only has to file a lab run that `uses` the diff.
|
|
77
77
|
|
|
78
78
|
Also decided: `REVIEW_BY_AUTHOR` covers the diff's author (built); `supersedes` joins artifacts (built).
|
|
79
|
+
|
|
80
|
+
**Verified failure (0.5.1).** When the instrument's run on the current head records a negative result ("1 of 2
|
|
81
|
+
checks pass"), `tests_pass_current` can never be met; a checked claim resting on that result settles the
|
|
82
|
+
requirement as a verified failure (`stage()` says so), and `requirements_verified_current` stays unmet. The record
|
|
83
|
+
holds why the change did not ship.
|
|
@@ -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.5.
|
|
19
|
+
__version__ = "0.5.1"
|
|
20
20
|
|
|
21
21
|
from .bundle import verify_bundle # noqa: E402
|
|
22
22
|
from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
|
|
@@ -558,7 +558,7 @@ class Graph:
|
|
|
558
558
|
if type == "artifact":
|
|
559
559
|
node_id = "a-" + str(body["sha256"]).lower()[:16]
|
|
560
560
|
if node_id in s.nodes:
|
|
561
|
-
return self.
|
|
561
|
+
return self._absorb_artifact(s, node_id, body, links, author, at=at)
|
|
562
562
|
nid = node_id or ("inv-" + secrets.token_hex(4) if type == "investigation"
|
|
563
563
|
else "n" + secrets.token_hex(6))
|
|
564
564
|
if nid in s.nodes:
|
|
@@ -773,6 +773,50 @@ class Graph:
|
|
|
773
773
|
if lines:
|
|
774
774
|
out["orders"] = lines
|
|
775
775
|
|
|
776
|
+
def _absorb_artifact(self, s: D.Snap, nid: str, body: dict, links: list, author: str, *, at: str = "") -> dict:
|
|
777
|
+
"""The same bytes are one node. A second registration adds what the first lacked: the links given now
|
|
778
|
+
(part_of a question, supersedes an older file) and, when the node was untyped, its kind / meta / path — a
|
|
779
|
+
classification of the bytes, by anyone, as a new version (the bytes never change). Called inside create()'s
|
|
780
|
+
lock; the caller's finally refreshes the snapshot."""
|
|
781
|
+
n = s.nodes[nid]
|
|
782
|
+
at = self._when(author, at)
|
|
783
|
+
have = {(e["rel"], e["dst"]) for e in s.out.get(nid, [])} | {(e["rel"], e["src"]) for e in s.inn.get(nid, [])}
|
|
784
|
+
oriented = [self._orient(s, *self._link_spec(nid, l)) for l in links]
|
|
785
|
+
pending = [(a, rel, b, l) for (a, rel, b, _f), l in zip(oriented, links)
|
|
786
|
+
if (rel, b if a == nid else a) not in have]
|
|
787
|
+
for a, rel, b, _l in pending:
|
|
788
|
+
self._check_link(s, a, rel, b)
|
|
789
|
+
merged = None
|
|
790
|
+
if not n["body"].get("kind") and body.get("kind"):
|
|
791
|
+
merged = clean_body("artifact", {**n["body"], **{k: v for k, v in body.items()
|
|
792
|
+
if k in ("kind", "meta", "path", "format", "size") and v}})
|
|
793
|
+
if author not in self.policy.curators:
|
|
794
|
+
np_ = profiles.node_issues(s, n["inv"], "artifact", merged, [(a, rel, b) for a, rel, b, _l in pending])
|
|
795
|
+
if np_:
|
|
796
|
+
raise GraphError("artifact refused:\n- " + "\n- ".join(p.text() for p in np_), [p.to_dict() for p in np_])
|
|
797
|
+
if not pending and merged is None:
|
|
798
|
+
return self.get(nid)
|
|
799
|
+
|
|
800
|
+
def write(con):
|
|
801
|
+
if merged is not None:
|
|
802
|
+
v = con.execute("SELECT MAX(v) FROM versions WHERE node=?", (nid,)).fetchone()[0] + 1
|
|
803
|
+
sha = self._version_row(con, nid, v, at, author, f"typed as {merged.get('kind')}", merged)
|
|
804
|
+
self._event(con, author, "revise", nid, {"v": v, "reason": f"typed as {merged.get('kind')}",
|
|
805
|
+
"body_sha": sha}, at)
|
|
806
|
+
for a, rel, b, l in pending:
|
|
807
|
+
self._insert_edge(con, s, a, rel, b, l.get("data") or {}, l.get("why") or "", author, at)
|
|
808
|
+
self._transaction(write)
|
|
809
|
+
self._after_write()
|
|
810
|
+
out = self.get(nid)
|
|
811
|
+
notes = ["the same bytes were already registered: one node"]
|
|
812
|
+
if merged is not None:
|
|
813
|
+
notes.append(f"typed as {merged.get('kind')} (v{out['v']})")
|
|
814
|
+
if pending:
|
|
815
|
+
notes.append("linked: " + ", ".join(f"{rel} {s.nodes[b if a == nid else a].get('code') or b}"
|
|
816
|
+
for a, rel, b, _l in pending))
|
|
817
|
+
out["notes"] = notes
|
|
818
|
+
return out
|
|
819
|
+
|
|
776
820
|
def _may_revise(self, s: D.Snap, author: str, n: dict) -> bool:
|
|
777
821
|
return author in {n["created_by"]} | self.policy.curators or author in D.board(s, n["id"])
|
|
778
822
|
|
|
@@ -229,7 +229,8 @@ def _brief_min(s: D.Snap, i: str) -> dict:
|
|
|
229
229
|
m = s.nodes[i]
|
|
230
230
|
b = m["body"]
|
|
231
231
|
return {"id": i, "code": m.get("code", ""), "type": m["type"],
|
|
232
|
-
"title": str(b.get("title") or b.get("statement") or b.get("text") or b.get("summary") or
|
|
232
|
+
"title": str(b.get("title") or b.get("statement") or b.get("text") or b.get("summary") or b.get("aim")
|
|
233
|
+
or b.get("choice") or b.get("citation") or b.get("name") or b.get("path") or i)[:160]}
|
|
233
234
|
|
|
234
235
|
|
|
235
236
|
def report(g: "Graph", inv: str) -> dict:
|
|
@@ -347,6 +347,7 @@ OPTIMIZATION2 = _add(Profile(
|
|
|
347
347
|
_S("solved", "a result: objective, feasible, bound / gap (or infeasible)", "solved", "solver, instrument"),
|
|
348
348
|
_S("verified", "a verifier recomputed feasibility and checked the result", "result_reviewed", "verifier"),
|
|
349
349
|
_S("claimed", "a claim: best found with its gap, or optimal with its bound", "claim_made", "solver"),
|
|
350
|
+
_S("checked", "the verifier checked the claim's wording against the result", "reviewed", "verifier"),
|
|
350
351
|
_S("recommended", "a decision: the incumbent to use, with its sensitivity", "decided", "lead", optional=True)),
|
|
351
352
|
invalidates=("uses",),
|
|
352
353
|
artifacts={"instance": (), "solution": ("objective", "feasible"), "model": ("solver",)},
|
|
@@ -27,6 +27,7 @@ from . import language as L
|
|
|
27
27
|
from .checks import Problem
|
|
28
28
|
from .profiles import (_EDGE, _OPTIMAL, _PASSES, Profile, _cites_source, _has, _instrument_backed, _lab_positive,
|
|
29
29
|
_live, _producing_run, _vals, profile_of)
|
|
30
|
+
from .schema import POSITIVE
|
|
30
31
|
|
|
31
32
|
_LIVE_ORDER = re.compile(r"\b(live (order|trade|position)|market order|limit order|place[ds]? (an? )?order|"
|
|
32
33
|
r"execute[sd]? (a |the )?trade|sen[dt] (it |the order )?to (the )?(exchange|broker|venue)|"
|
|
@@ -210,9 +211,21 @@ def g_claim_made(sc: Scope, now: str) -> dict:
|
|
|
210
211
|
return _g(bool(cs), cs, f"no live claim answers {sc.code(sc.root)}")
|
|
211
212
|
|
|
212
213
|
|
|
214
|
+
def _checked_since(sc: Scope, nid: str) -> bool:
|
|
215
|
+
"""Checked — and, under an order, by a review written since the order (the node itself may well predate it:
|
|
216
|
+
a review order's target is the claim or result it is to review)."""
|
|
217
|
+
s = sc.s
|
|
218
|
+
if (D.state(s, nid).get("review") or {}).get("state") != "checked":
|
|
219
|
+
return False
|
|
220
|
+
if not sc.since:
|
|
221
|
+
return True
|
|
222
|
+
return any(e["rel"] == "reviews" and s.nodes[e["src"]]["created_at"] >= sc.since
|
|
223
|
+
and s.nodes[e["src"]]["body"].get("verdict") in POSITIVE for e in s.inn.get(nid, []))
|
|
224
|
+
|
|
225
|
+
|
|
213
226
|
def g_reviewed(sc: Scope, now: str) -> dict:
|
|
214
227
|
s = sc.s
|
|
215
|
-
checked = [c for c in sc.
|
|
228
|
+
checked = [c for c in sc.claims if _live(s, c) and _checked_since(sc, c)]
|
|
216
229
|
fresh = [c for c in checked if not D.stale(s, c)]
|
|
217
230
|
if fresh:
|
|
218
231
|
return _g(True, fresh, "")
|
|
@@ -224,7 +237,8 @@ def g_reviewed(sc: Scope, now: str) -> dict:
|
|
|
224
237
|
|
|
225
238
|
|
|
226
239
|
def g_decided(sc: Scope, now: str) -> dict:
|
|
227
|
-
|
|
240
|
+
# a decision that settles a work order is the harness accepting work, not a decision on the question
|
|
241
|
+
ds = [d for d in sc.fresh(sc.decisions) if not any(e["rel"] == "settles" for e in sc.s.out.get(d, []))]
|
|
228
242
|
return _g(bool(ds), ds, f"no decision is based on a claim or result on {sc.code(sc.root)}")
|
|
229
243
|
|
|
230
244
|
|
|
@@ -238,8 +252,9 @@ def _current_diffs(sc: Scope, kind: str) -> tuple[list[str], list[str]]:
|
|
|
238
252
|
|
|
239
253
|
|
|
240
254
|
def g_diff_registered(sc: Scope, now: str) -> dict:
|
|
255
|
+
# the question's current diff is a state, not work since the order: a change registered before the order (the
|
|
256
|
+
# task's own files are artifacts from the start, and the same bytes are one node) meets it
|
|
241
257
|
live, _old = _current_diffs(sc, "diff")
|
|
242
|
-
live = [a for a in live if not sc.since or sc.s.nodes[a]["created_at"] >= sc.since]
|
|
243
258
|
return _g(bool(live), live[:1], f"no live artifact kind=diff (meta: base_sha, head_sha, files) part_of "
|
|
244
259
|
f"{sc.code(sc.root)}")
|
|
245
260
|
|
|
@@ -331,12 +346,18 @@ def _positions(sc: Scope) -> list[str]:
|
|
|
331
346
|
return [d for d in sc.decisions if _live(sc.s, d) and (sc.s.nodes[d]["body"].get("kind") or "") == "position"]
|
|
332
347
|
|
|
333
348
|
|
|
349
|
+
def _no_positions(sc: Scope) -> list[str]:
|
|
350
|
+
"""Decisions not to trade (kind=no_position): a decision too — the thesis was judged and declined."""
|
|
351
|
+
return [d for d in sc.decisions if _live(sc.s, d) and (sc.s.nodes[d]["body"].get("kind") or "") == "no_position"]
|
|
352
|
+
|
|
353
|
+
|
|
334
354
|
def g_positioned(sc: Scope, now: str) -> dict:
|
|
335
355
|
s = sc.s
|
|
336
356
|
ds = [d for d in _positions(sc) if (not sc.since or s.nodes[d]["created_at"] >= sc.since)
|
|
337
357
|
and not _position_missing(s.nodes[d]["body"])]
|
|
338
|
-
|
|
339
|
-
|
|
358
|
+
ds += [d for d in _no_positions(sc) if not sc.since or s.nodes[d]["created_at"] >= sc.since]
|
|
359
|
+
return _g(bool(ds), ds, f"no decision based on a claim on {sc.code(sc.root)}: kind=position (size, stop, "
|
|
360
|
+
"review_date, paper=true), or kind=no_position when the thesis is declined")
|
|
340
361
|
|
|
341
362
|
|
|
342
363
|
def _past_review(sc: Scope, d: str, now: str) -> bool:
|
|
@@ -378,7 +399,7 @@ def g_solved(sc: Scope, now: str) -> dict:
|
|
|
378
399
|
|
|
379
400
|
def g_result_reviewed(sc: Scope, now: str) -> dict:
|
|
380
401
|
s = sc.s
|
|
381
|
-
rs = [r for r in sc.
|
|
402
|
+
rs = [r for r in sc.results if _live(s, r) and _checked_since(sc, r)]
|
|
382
403
|
return _g(bool(rs), rs, f"no result on {sc.code(sc.root)} is checked by a reviewer")
|
|
383
404
|
|
|
384
405
|
|
|
@@ -401,6 +422,25 @@ def evaluate_predicate(p: str, s: D.Snap, t: str, at: str, inv: str, who: str =
|
|
|
401
422
|
return GATES[p](Scope(s, t, since=at, who=who), s.now)
|
|
402
423
|
|
|
403
424
|
|
|
425
|
+
def _claim_results(s: D.Snap, c: str) -> list[str]:
|
|
426
|
+
rs = [e["src"] for e in s.inn.get(c, []) if e["rel"] == "supports" and s.nodes[e["src"]]["type"] == "result"]
|
|
427
|
+
rs += [e["dst"] for e in s.out.get(c, []) if e["rel"] == "derived_from" and s.nodes[e["dst"]]["type"] == "result"]
|
|
428
|
+
return _uniq(rs)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _lab_negative(s: D.Snap, r: str) -> bool:
|
|
432
|
+
"""The instrument stands behind a failure: the result's own lab run, or a live lab run replicating the run that
|
|
433
|
+
produced it, has a live result with outcome negative."""
|
|
434
|
+
run = _producing_run(s, r)
|
|
435
|
+
if not run:
|
|
436
|
+
return False
|
|
437
|
+
labs = [run] if s.nodes[run]["body"].get("kind") == "lab" else []
|
|
438
|
+
labs += [e["src"] for e in s.inn.get(run, []) if e["rel"] == "replicates" and s.nodes[e["src"]]["body"].get("kind") == "lab"]
|
|
439
|
+
return any(_live(s, lab) and any(s.nodes[e["dst"]]["type"] == "result" and s.nodes[e["dst"]]["body"].get("outcome") == "negative"
|
|
440
|
+
and _live(s, e["dst"]) for e in s.out.get(lab, []) if e["rel"] == "produced")
|
|
441
|
+
for lab in labs)
|
|
442
|
+
|
|
443
|
+
|
|
404
444
|
# ── the derived stage ──────────────────────────────────────────────
|
|
405
445
|
|
|
406
446
|
def stage_of(s: D.Snap, t: str, now: str = "") -> dict:
|
|
@@ -422,6 +462,23 @@ def stage_of(s: D.Snap, t: str, now: str = "") -> dict:
|
|
|
422
462
|
if stage is None and not r["met"] and not st.optional:
|
|
423
463
|
stage, nxt = (i, st.name), r["why"]
|
|
424
464
|
stages = p.workflow.stages
|
|
465
|
+
# An honest "it can't be answered" ends a question whatever its stage: a checked claim of kind inconclusive
|
|
466
|
+
# (or exclusion) that answers it — no source found, too few trades, infeasible. The record holds the reason.
|
|
467
|
+
checked = [c for c in sc.claims if _live(s, c) and not D.stale(s, c)
|
|
468
|
+
and (D.state(s, c).get("review") or {}).get("state") == "checked"]
|
|
469
|
+
settled = [(c, f"{s.nodes[c]['body'].get('kind')} answer, checked") for c in checked
|
|
470
|
+
if (s.nodes[c]["body"].get("kind") or "finding") in ("inconclusive", "exclusion")]
|
|
471
|
+
# A verified failure settles a requirement too: a checked claim resting on an instrument-recorded negative
|
|
472
|
+
# result ("1 of 2 checks pass", re-run by the instrument) — the gate that asks for a passing run can never be met,
|
|
473
|
+
# and the record now holds why. Only workflows whose gate is an instrument pass have this ending.
|
|
474
|
+
if any(st.gate == "tests_pass_current" for st in stages):
|
|
475
|
+
settled += [(c, "verified failure: an instrument-recorded negative result, checked") for c in checked
|
|
476
|
+
if any(_lab_negative(s, r) for r in _claim_results(s, c))]
|
|
477
|
+
if stage is not None and settled:
|
|
478
|
+
rv = next((i for i, st in enumerate(stages) if st.gate == "reviewed"), len(stages) - 1)
|
|
479
|
+
return {**base, "workflow": True, "stage": stages[rv].name, "index": rv + 1, "of": len(stages),
|
|
480
|
+
"complete": True, "next": "", "gates": gates, "settled_by": sc.code(settled[0][0]),
|
|
481
|
+
"settled_how": settled[0][1]}
|
|
425
482
|
if stage is None: # every required gate met: the stage is the last one whose gate is
|
|
426
483
|
last = max(i for i, gt in enumerate(gates) if gt["met"])
|
|
427
484
|
return {**base, "workflow": True, "stage": stages[last].name, "index": last + 1, "of": len(stages),
|
|
@@ -541,6 +598,25 @@ def review_refusal(p: Profile, s: D.Snap, target: str, author: str) -> str:
|
|
|
541
598
|
|
|
542
599
|
# ── completion criteria the workflows add ──────────────────────────
|
|
543
600
|
|
|
601
|
+
def _backed_fresh(s: D.Snap, r: str) -> bool:
|
|
602
|
+
"""A result the instrument stands behind on the current artifacts: its own lab run, or a live lab run that
|
|
603
|
+
replicates the run that produced it with a positive result — and every artifact that lab run used is live, and
|
|
604
|
+
the result itself is not stale."""
|
|
605
|
+
if D.stale(s, r):
|
|
606
|
+
return False
|
|
607
|
+
run = _producing_run(s, r)
|
|
608
|
+
if not run:
|
|
609
|
+
return False
|
|
610
|
+
labs = [run] if s.nodes[run]["body"].get("kind") == "lab" else []
|
|
611
|
+
labs += [e["src"] for e in s.inn.get(run, []) if e["rel"] == "replicates" and s.nodes[e["src"]]["body"].get("kind") == "lab"]
|
|
612
|
+
for lab in labs:
|
|
613
|
+
if not _live(s, lab) or not _positive_results(s, lab):
|
|
614
|
+
continue
|
|
615
|
+
if all(D.lifecycle(s, a) == "active" for a in _uses(s, lab)):
|
|
616
|
+
return True
|
|
617
|
+
return False
|
|
618
|
+
|
|
619
|
+
|
|
544
620
|
def _checked_claims(s: D.Snap, q: str) -> list[str]:
|
|
545
621
|
return [e["src"] for e in s.inn.get(q, []) if e["rel"] == "answers" and s.nodes[e["src"]]["type"] == "claim"
|
|
546
622
|
and _live(s, e["src"]) and (D.state(s, e["src"]).get("review") or {}).get("state") == "checked"]
|
|
@@ -557,7 +633,7 @@ def criterion(name: str, s: D.Snap, inv: str) -> dict:
|
|
|
557
633
|
for c in _checked_claims(s, q): # any checked answer resting on a fresh instrument run
|
|
558
634
|
rs = [e["src"] for e in s.inn.get(c, []) if e["rel"] == "supports"] + \
|
|
559
635
|
[e["dst"] for e in s.out.get(c, []) if e["rel"] == "derived_from"]
|
|
560
|
-
if any(s.nodes[r]["type"] == "result" and
|
|
636
|
+
if any(s.nodes[r]["type"] == "result" and _backed_fresh(s, r) for r in rs):
|
|
561
637
|
ok = True
|
|
562
638
|
break
|
|
563
639
|
if not ok:
|
|
@@ -566,10 +642,14 @@ def criterion(name: str, s: D.Snap, inv: str) -> dict:
|
|
|
566
642
|
"current head" if qs and not bad else "no current instrument-run evidence for: " + ", ".join(bad)}
|
|
567
643
|
if name == "position_sized":
|
|
568
644
|
ds = [i for i, n in s.nodes.items() if n["inv"] == inv and n["type"] == "decision" and _live(s, i)
|
|
569
|
-
and (
|
|
570
|
-
and
|
|
571
|
-
|
|
572
|
-
|
|
645
|
+
and any(e["rel"] == "based_on" for e in s.out.get(i, []))
|
|
646
|
+
and (((n["body"].get("kind") or "") == "position" and not _position_missing(n["body"]))
|
|
647
|
+
or (n["body"].get("kind") or "") == "no_position")]
|
|
648
|
+
taken = sum(1 for d in ds if s.nodes[d]["body"].get("kind") == "position")
|
|
649
|
+
return {"met": bool(ds), "why": f"{taken} paper position(s) with size, stop and review date, "
|
|
650
|
+
f"{len(ds) - taken} declined" if ds else
|
|
651
|
+
"no decision based on a claim: kind=position with size, stop, review_date and paper=true, or "
|
|
652
|
+
"kind=no_position"}
|
|
573
653
|
if name == "nothing_past_review":
|
|
574
654
|
late = []
|
|
575
655
|
for i, n in s.nodes.items():
|
|
@@ -361,3 +361,93 @@ def test_server_serves_stages():
|
|
|
361
361
|
assert st == 200 and body["code"] == "Q1" and [x["gate"] for x in body["gates"]][:3] == ["stated", "diff_registered", "tests_pass_current"]
|
|
362
362
|
st, body = dispatch(g, "GET", "/profiles")
|
|
363
363
|
assert st == 200 and body["trading@2"]["workflow"]["decisions"] == {"position": ["size", "stop", "review_date", "paper"]}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# ── 0.5.1: declining a thesis is a decision; an honest "can't be answered" settles a question ──
|
|
367
|
+
|
|
368
|
+
def test_no_position_is_a_decision():
|
|
369
|
+
g, inv, q = _g("trading@2", reviewers=("risk",))
|
|
370
|
+
h = g.create(author="quant", type="hypothesis", inv=inv, fields={"statement": "Momentum over 12 months persists for a month.",
|
|
371
|
+
"predictions": ["the top decile outperforms after costs out of sample"]}, links=[{"rel": "answers", "to": q}])["id"]
|
|
372
|
+
x = g.create(author="quant", type="experiment", inv=inv, fields={"aim": "Backtest 12-1 momentum.",
|
|
373
|
+
"protocol": "In sample 2015-2021, out of sample 2022-2024; fees 10 bp, slippage 5 bp; 24 configurations.",
|
|
374
|
+
"success_criteria": "positive out-of-sample return after costs"},
|
|
375
|
+
links=[{"rel": "tests", "to": h, "data": {"expected": "positive oos return after costs"}}])["id"]
|
|
376
|
+
rs = _bt(g, inv, "quant", {"oos_return": {"value": -0.02}, "fees": {"value": 0.002}, "trials": {"value": 24}},
|
|
377
|
+
summary="Backtested the thesis out of sample: it loses after costs.", q=q, x=x)
|
|
378
|
+
c = g.create(author="quant", type="claim", inv=inv, fields={"statement": "The thesis has no edge out of sample: -0.02 a year after fees.",
|
|
379
|
+
"scope": "Daily bars, 2022–2024 held out, after fees, 24 configurations tried."},
|
|
380
|
+
links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs}])["id"]
|
|
381
|
+
g.review(author="risk", target=c, verdict="checked", method="Re-ran the OOS window.", notes="Negative after costs, as stated.")
|
|
382
|
+
assert _stage(g, q) == ("positioned", 7, False)
|
|
383
|
+
g.create(author=H, type="decision", inv=inv, fields={"choice": "No position: the thesis loses after costs out of sample.",
|
|
384
|
+
"reasons": "A negative out-of-sample return after costs is not a thesis to fund.", "kind": "no_position"},
|
|
385
|
+
links=[{"rel": "based_on", "to": c}])
|
|
386
|
+
assert g.stage(q)["complete"]
|
|
387
|
+
comp = {x_["name"]: x_ for x_ in g.completion(inv)["criteria"]}
|
|
388
|
+
assert comp["position_sized"]["met"] and "1 declined" in comp["position_sized"]["why"] and comp["nothing_past_review"]["met"]
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def test_a_checked_inconclusive_answer_settles_a_question_without_a_source():
|
|
392
|
+
g, inv, q = _g("knowledge@2")
|
|
393
|
+
assert _stage(g, q) == ("sourced", 3, False)
|
|
394
|
+
# the search is a run; what it found (nothing) is a result; the honest answer derives from it
|
|
395
|
+
run = g.create(author="r", type="run", inv=inv, fields={"kind": "agent", "name": "search", "state": "succeeded"})["id"]
|
|
396
|
+
rs = g.create(author="r", type="result", inv=inv, fields={"summary": "Read 3 of 3 pages on the host; 0 mention decommissioning.",
|
|
397
|
+
"values": {"pages_read": {"k": 3, "n": 3}, "mentions": {"value": 0}}, "outcome": "negative", "kind": "observed"},
|
|
398
|
+
links=[{"rel": "produced", "from": run}])["id"]
|
|
399
|
+
c = g.create(author="r", type="claim", inv=inv, fields={"statement": "No page on the source host says when the dam was decommissioned.",
|
|
400
|
+
"scope": "The 3 of 3 pages on the source host, read in full.", "kind": "inconclusive"},
|
|
401
|
+
links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs}])["id"]
|
|
402
|
+
assert _stage(g, q) == ("sourced", 3, False) # a claim alone settles nothing
|
|
403
|
+
g.review(author="critic", target=c, verdict="checked", method="Read the three pages.", notes="Nothing on decommissioning.")
|
|
404
|
+
x = g.stage(q)
|
|
405
|
+
assert x["complete"] and x["stage"] == "checked" and x["settled_by"] == "CL1" and "inconclusive" in x["settled_how"]
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def test_accepting_an_order_is_not_the_questions_decision():
|
|
409
|
+
g, inv, q = _g("scientific@2")
|
|
410
|
+
o = g.issue_work(author=H, inv=inv, target=q, assignee="w", objective="State a hypothesis for Q1.", done_when=[{"p": "hypothesis_stated"}])
|
|
411
|
+
g.create(author="w", type="hypothesis", inv=inv, fields={"statement": "Caching halves the latency.", "predictions": ["p50 halves"]},
|
|
412
|
+
links=[{"rel": "answers", "to": q}])
|
|
413
|
+
g.return_work(author="w", order=o["order"], note="stated")
|
|
414
|
+
assert g.settle(author=H, inv=inv) # a decision that settles the order, based on H1
|
|
415
|
+
assert not _gate(g, q, "decided")["met"] # … is the harness accepting work, not a decision on Q1
|
|
416
|
+
assert "X1" not in g.report(inv)["markdown"] or "n" not in g.report(inv)["markdown"]
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def test_registering_the_same_bytes_again_types_and_links_the_one_node():
|
|
420
|
+
g, inv, q = _g("software@2")
|
|
421
|
+
a = g.create(author=H, type="artifact", inv=inv, fields={"sha256": SHA_A, "path": "mod.py", "size": 20}) # the task's file
|
|
422
|
+
assert a["body"].get("kind") is None and not _gate(g, q, "diff_registered")["met"]
|
|
423
|
+
b = g.create(author="dev", type="artifact", inv=inv, fields={"sha256": SHA_A, "path": "mod.py", "kind": "diff",
|
|
424
|
+
"meta": {"base_sha": "0000000", "head_sha": "1111111", "files": ["mod.py"]}}, links=[{"rel": "part_of", "to": q}])
|
|
425
|
+
assert b["id"] == a["id"] and b["v"] == 2 and b["body"]["kind"] == "diff" and "typed as diff" in " ".join(b["notes"])
|
|
426
|
+
assert any(e["rel"] == "part_of" for e in b["out"])
|
|
427
|
+
assert _gate(g, q, "diff_registered")["met"] and _gate(g, q, "diff_registered")["evidence"] == ["A1"]
|
|
428
|
+
c = g.create(author="dev", type="artifact", inv=inv, fields={"sha256": SHA_A, "kind": "diff"}, links=[{"rel": "part_of", "to": q}])
|
|
429
|
+
assert c["v"] == 2 and len([e for e in c["out"] if e["rel"] == "part_of"]) == 1 # idempotent: nothing added twice
|
|
430
|
+
with pytest.raises(GraphError, match="ARTIFACT_MISSING_FIELDS"):
|
|
431
|
+
g.create(author="dev", type="artifact", inv=inv, fields={"sha256": SHA_B, "path": "x.py"})
|
|
432
|
+
g.create(author="dev", type="artifact", inv=inv, fields={"sha256": SHA_B, "path": "x.py", "kind": "diff"})
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def test_a_verified_failure_settles_a_software_requirement():
|
|
436
|
+
g, inv, q = _g("software@2")
|
|
437
|
+
a1 = _diff(g, inv, q, "dev", SHA_A, "1111111")
|
|
438
|
+
run = g.create(author="tester", type="run", inv=inv, fields={"kind": "agent", "name": "checks", "state": "succeeded"},
|
|
439
|
+
links=[{"rel": "uses", "to": a1, "data": {"role": "code"}}])["id"]
|
|
440
|
+
rs = g.create(author="tester", type="result", inv=inv, fields={"summary": "Ran the checks: 1 of 2 pass.",
|
|
441
|
+
"values": {"passed": {"k": 1, "n": 2}}, "outcome": "negative"}, links=[{"rel": "produced", "from": run}])["id"]
|
|
442
|
+
lab = g.create(author="lab", type="run", inv=inv, fields={"kind": "lab", "name": "re-run", "state": "succeeded"},
|
|
443
|
+
links=[{"rel": "replicates", "to": run, "data": {"differs": "same_code"}}, {"rel": "uses", "to": a1, "data": {"role": "code"}}])["id"]
|
|
444
|
+
g.create(author="lab", type="result", inv=inv, fields={"summary": "Re-ran the checks: 1 of 2 pass.",
|
|
445
|
+
"values": {"passed": {"k": 1, "n": 2}}, "outcome": "negative"}, links=[{"rel": "produced", "from": lab}])
|
|
446
|
+
c = g.create(author="tester", type="claim", inv=inv, fields={"statement": "The change does not meet the requirement: 1 of 2 checks pass.",
|
|
447
|
+
"scope": "mod.py at head 1111111."}, links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs}])["id"]
|
|
448
|
+
assert _stage(g, q) == ("verified", 3, False) # no passing run on the head, and there never will be
|
|
449
|
+
g.review(author="critic", target=c, verdict="checked", method="Read the lab run.", notes="One of two, as stated.")
|
|
450
|
+
x = g.stage(q)
|
|
451
|
+
assert x["complete"] and "verified failure" in x["settled_how"] and x["settled_by"] == "CL1"
|
|
452
|
+
assert not _gate(g, q, "tests_pass_current")["met"]
|
|
453
|
+
assert not next(c_ for c_ in g.completion(inv)["criteria"] if c_["name"] == "requirements_verified_current")["met"]
|
|
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
|
|
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
|
|
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
|