hypgraph 0.4.0__tar.gz → 0.4.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 (61) hide show
  1. {hypgraph-0.4.0 → hypgraph-0.4.1}/CHANGELOG.md +10 -0
  2. {hypgraph-0.4.0 → hypgraph-0.4.1}/PKG-INFO +1 -1
  3. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/__init__.py +1 -1
  4. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/checks.py +3 -0
  5. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/cli.py +1 -1
  6. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/coord.py +11 -1
  7. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/profiles.py +20 -2
  8. hypgraph-0.4.1/tests/test_profiles_041.py +69 -0
  9. {hypgraph-0.4.0 → hypgraph-0.4.1}/.gitignore +0 -0
  10. {hypgraph-0.4.0 → hypgraph-0.4.1}/AGENTS.md +0 -0
  11. {hypgraph-0.4.0 → hypgraph-0.4.1}/CITATION.cff +0 -0
  12. {hypgraph-0.4.0 → hypgraph-0.4.1}/LICENSE +0 -0
  13. {hypgraph-0.4.0 → hypgraph-0.4.1}/NOTICE +0 -0
  14. {hypgraph-0.4.0 → hypgraph-0.4.1}/README.md +0 -0
  15. {hypgraph-0.4.0 → hypgraph-0.4.1}/docs/PLAN.md +0 -0
  16. {hypgraph-0.4.0 → hypgraph-0.4.1}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
  17. {hypgraph-0.4.0 → hypgraph-0.4.1}/docs/decisions/ADR-002-governance-profiles.md +0 -0
  18. {hypgraph-0.4.0 → hypgraph-0.4.1}/docs/review-2026-10-24.md +0 -0
  19. {hypgraph-0.4.0 → hypgraph-0.4.1}/docs/spec.md +0 -0
  20. {hypgraph-0.4.0 → hypgraph-0.4.1}/pyproject.toml +0 -0
  21. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/__main__.py +0 -0
  22. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/bundle.py +0 -0
  23. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/demos/__init__.py +0 -0
  24. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/demos/broad_street.py +0 -0
  25. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/derive.py +0 -0
  26. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/graph.py +0 -0
  27. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/language.py +0 -0
  28. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/mcp.py +0 -0
  29. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/plan.py +0 -0
  30. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/policy.py +0 -0
  31. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/refusals.py +0 -0
  32. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/report_check.py +0 -0
  33. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/schema.py +0 -0
  34. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/server.py +0 -0
  35. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/skill/SKILL.md +0 -0
  36. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/stats.py +0 -0
  37. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/store.py +0 -0
  38. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/ui/__init__.py +0 -0
  39. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/ui/atlas.css +0 -0
  40. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/ui/atlas.js +0 -0
  41. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/ui/hypgraph.svg +0 -0
  42. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/ui/index.html +0 -0
  43. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
  44. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/ui/vendor/cose-base.js +0 -0
  45. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
  46. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
  47. {hypgraph-0.4.0 → hypgraph-0.4.1}/src/hypgraph/ui/vendor/layout-base.js +0 -0
  48. {hypgraph-0.4.0 → hypgraph-0.4.1}/studies/harmony/tests/test_suite.py +0 -0
  49. {hypgraph-0.4.0 → hypgraph-0.4.1}/tests/test_cancel_releases_lease.py +0 -0
  50. {hypgraph-0.4.0 → hypgraph-0.4.1}/tests/test_coord.py +0 -0
  51. {hypgraph-0.4.0 → hypgraph-0.4.1}/tests/test_d10.py +0 -0
  52. {hypgraph-0.4.0 → hypgraph-0.4.1}/tests/test_engine.py +0 -0
  53. {hypgraph-0.4.0 → hypgraph-0.4.1}/tests/test_engine_fixes.py +0 -0
  54. {hypgraph-0.4.0 → hypgraph-0.4.1}/tests/test_incremental.py +0 -0
  55. {hypgraph-0.4.0 → hypgraph-0.4.1}/tests/test_log_and_check.py +0 -0
  56. {hypgraph-0.4.0 → hypgraph-0.4.1}/tests/test_order_detail.py +0 -0
  57. {hypgraph-0.4.0 → hypgraph-0.4.1}/tests/test_profiles.py +0 -0
  58. {hypgraph-0.4.0 → hypgraph-0.4.1}/tests/test_refusals.py +0 -0
  59. {hypgraph-0.4.0 → hypgraph-0.4.1}/tests/test_server_auth.py +0 -0
  60. {hypgraph-0.4.0 → hypgraph-0.4.1}/tests/test_stats_values.py +0 -0
  61. {hypgraph-0.4.0 → hypgraph-0.4.1}/tests/test_surfaces.py +0 -0
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.1 — 2026-10-25
4
+
5
+ Three rules the hypswarm examples exposed, each with a test:
6
+ - A claim's numbers that appear in a cited source's passage, citation or title are accounted for (a knowledge claim
7
+ "signed on 24 October 1648" citing the passage that says so is no longer `UNBOUND_NUMBER`).
8
+ - A rework's predicates count evidence written since the **first** order of its chain (`coord.chain_root`): a
9
+ `tests_pass` met by the instrument's re-run under the original order stays met under the rework.
10
+ - `PASS_WITHOUT_INSTRUMENT_RUN` accepts a claim resting on the author's result when a live lab run `replicates`
11
+ the run that produced it and the re-run's result is positive (`profiles._instrument_backed`).
12
+
3
13
  ## 0.4.0 — 2026-10-25: the coordination layer
4
14
 
5
15
  Held until the harmony study had exercised every path (pilots 1–4, a 142-run confirmatory, two follow-up rounds). Governance profiles, token-bound authors, work orders with predicates, order text that carries the target, instrument reproduction, replayable refusals.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hypgraph
3
- Version: 0.4.0
3
+ Version: 0.4.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.4.0"
19
+ __version__ = "0.4.1"
20
20
 
21
21
  from .bundle import verify_bundle # noqa: E402
22
22
  from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
@@ -385,6 +385,9 @@ def claim_issues(s: Snap, body: dict, support: list[str], inv: str) -> list[Prob
385
385
  plain, stat = derived_numbers(bound)
386
386
  allowed = plain + [v for v, *_ in numbers(scope)] + \
387
387
  [float(c) for c in (invb.get("constants") or []) if isinstance(c, (int, float))]
388
+ for src in support: # a cited source's own text accounts for the numbers in it
389
+ sb = s.nodes[src]["body"] if s.nodes[src]["type"] == "source" else {}
390
+ allowed += [v for v, *_ in numbers(" ".join(str(sb.get(k) or "") for k in ("passage", "citation", "title")))]
388
391
  if _CONVENTIONS.search(clean): # "95% interval", "α 0.05", "80% power" are conventions
389
392
  allowed += [95.0, 99.0, 90.0, 80.0, 0.05, 0.01]
390
393
  loose = [p for p in numbers(clean) if not _matches(p, allowed) and not _matches_stat(p, stat)]
@@ -515,7 +515,7 @@ def build_parser() -> argparse.ArgumentParser:
515
515
  s.add_argument("file", help="the markdown file ('-' for stdin)")
516
516
  s.add_argument("--inv", default="", help="the investigation it reports on (default: inferred)")
517
517
  s.add_argument("--strict", action="store_true", help="warnings (e.g. unsourced numbers) fail too")
518
- s = cmd("bound", cmd_bound, "what k of n actually supports: the 95% bounds behind any 'never'")
518
+ s = cmd("bound", cmd_bound, "what k of n actually supports: the 95%% bounds behind any 'never'")
519
519
  s.add_argument("k", type=int, help="how many times it happened")
520
520
  s.add_argument("n", type=int, help="how many trials")
521
521
  s.add_argument("--exposure", type=float, default=0, help="how many uses / users / units it will apply to")
@@ -235,10 +235,20 @@ def _claims_on(s: D.Snap, t: str) -> list[str]:
235
235
  return list(dict.fromkeys(out))
236
236
 
237
237
 
238
+ def chain_root(s: D.Snap, oid: str) -> str:
239
+ """The first order of a rework chain (follows ``supersedes``); an order that replaced nothing is its own root."""
240
+ seen = [oid]
241
+ while True:
242
+ older = [e["dst"] for e in s.out.get(seen[-1], []) if e["rel"] == "supersedes"]
243
+ if not older or older[0] in seen:
244
+ return seen[-1]
245
+ seen.append(older[0])
246
+
247
+
238
248
  def _eval(g: "Graph", s: D.Snap, oid: str, pred: dict, text: str) -> dict:
239
249
  p = pred.get("p", "")
240
250
  t = target_of(s, oid)
241
- at = s.nodes[oid]["created_at"]
251
+ at = s.nodes[chain_root(s, oid)]["created_at"] # a rework keeps what its earlier attempts wrote
242
252
  res = {"p": p, "met": False, "evidence": [], "why": ""}
243
253
  if p in profiles.PRED_DESC:
244
254
  return res | profiles.evaluate(p, s, oid, t, at, s.nodes[oid]["body"].get("assignee", ""), s.nodes[oid]["inv"])
@@ -200,6 +200,23 @@ def _lab_positive(s: D.Snap, rid: str) -> bool:
200
200
  return bool(r) and s.nodes[r]["body"].get("kind") == "lab" and s.nodes[rid]["body"].get("outcome") == "positive"
201
201
 
202
202
 
203
+ def _instrument_backed(s: D.Snap, rid: str) -> bool:
204
+ """A result is instrument-backed when it comes from a lab run, or when a lab run replicates the run that
205
+ produced it and that re-run's own result is positive (the instrument re-executed the author's code)."""
206
+ if _lab_positive(s, rid):
207
+ return True
208
+ r = _producing_run(s, rid)
209
+ if not r:
210
+ return False
211
+ for lab in (e["src"] for e in s.inn.get(r, []) if e["rel"] == "replicates"):
212
+ if s.nodes[lab]["body"].get("kind") != "lab" or not _live(s, lab):
213
+ continue
214
+ if any(e["rel"] == "produced" and s.nodes[e["dst"]]["body"].get("outcome") == "positive"
215
+ for e in s.out.get(lab, [])):
216
+ return True
217
+ return False
218
+
219
+
203
220
  def _bearing(s: D.Snap, t: str, ntype: str) -> list[str]:
204
221
  """Nodes of a type linked to the target by any relation, either way."""
205
222
  out = [e["src"] for e in s.inn.get(t, []) if s.nodes[e["src"]]["type"] == ntype]
@@ -287,11 +304,12 @@ def claim_issues(s: D.Snap, inv: str, body: dict, support: list, pending: list)
287
304
  "or gap — an incumbent without a bound is a solution, not an optimum",
288
305
  fix="bind a result with bound or gap, or say 'best found'"))
289
306
  if "pass_needs_instrument" in p.validators and L.asserted(_PASSES, stmt):
290
- ok = any(_lab_positive(s, r) for r in results)
307
+ ok = any(_instrument_backed(s, r) for r in results)
291
308
  if not ok:
292
309
  out.append(Problem("PASS_WITHOUT_INSTRUMENT_RUN", "'passes' is asserted, but no supporting result comes "
293
310
  "from an instrument-recorded run — tests must be run by the instrument, not reported",
294
- fix="register_run with the test command as code; the instrument's re-run is the evidence"))
311
+ fix="register_run with the test command as code; the instrument's re-run is the evidence — write the "
312
+ "claim once the re-run has landed (the order's reply names it)"))
295
313
  if "claim_needs_source" in p.validators and kind in ("finding", "exclusion"):
296
314
  cites = any(rel == "cites" and s.nodes.get(b, {}).get("type") == "source" for a, rel, b in pending) or \
297
315
  any(_cites_source(s, x) for x in support if x in s.nodes and s.nodes[x]["type"] in ("claim", "result"))
@@ -0,0 +1,69 @@
1
+ """0.4.1: three rules the hypswarm examples exposed. A cited source accounts for the numbers in its passage; a
2
+ rework keeps the evidence its chain already produced; a claim of passing tests may rest on the author's result once
3
+ the instrument's re-run of that run is positive."""
4
+ import time
5
+
6
+ from hypgraph import Graph, GraphError, Policy
7
+ from hypgraph.coord import chain_root
8
+
9
+ import pytest
10
+
11
+ H = "harness"
12
+
13
+
14
+ def _g(profile):
15
+ g = Graph(policy=Policy(reviewers={"critic"}, extra_reserved={H}, instrument="lab"))
16
+ inv = g.create(author=H, type="investigation", fields={"title": "A profiled investigation.",
17
+ "objective": "See the 0.4.1 rules hold.", "profile": profile})["id"]
18
+ q = g.create(author=H, type="question", inv=inv, code="Q1", fields={"text": "When was it signed?"})["id"]
19
+ return g, inv, q
20
+
21
+
22
+ def test_a_cited_source_accounts_for_its_numbers():
23
+ g, inv, q = _g("knowledge@1")
24
+ src = g.create(author="r", type="source", inv=inv, fields={"citation": "Britannica, 'Peace of Westphalia'.",
25
+ "url": "https://www.britannica.com/event/Peace-of-Westphalia",
26
+ "passage": "the treaties were signed on October 24, 1648"})["id"]
27
+ c = g.create(author="r", type="claim", inv=inv, fields={"statement": "The Peace of Westphalia was signed on 24 October 1648.",
28
+ "scope": "The treaties of Osnabrück and Münster.", "kind": "finding"},
29
+ links=[{"rel": "answers", "to": q}, {"rel": "cites", "to": src}])
30
+ assert c["code"].startswith("CL")
31
+ with pytest.raises(GraphError, match="UNBOUND_NUMBER"): # a number the source does not contain
32
+ g.create(author="r", type="claim", inv=inv, fields={"statement": "It was signed by 109 delegations.",
33
+ "scope": "The treaties of Osnabrück and Münster.", "kind": "finding"},
34
+ links=[{"rel": "answers", "to": q}, {"rel": "cites", "to": src}])
35
+
36
+
37
+ def test_a_rework_keeps_its_chains_evidence():
38
+ g, inv, q = _g("software@1")
39
+ o = g.issue_work(author=H, inv=inv, target=q, assignee="tester", objective="Run the tests.")
40
+ time.sleep(1.1)
41
+ r = g.create(author="tester", type="run", inv=inv, fields={"kind": "agent", "name": "pytest", "state": "succeeded"})
42
+ rs = g.create(author="tester", type="result", inv=inv, fields={"summary": "Ran pytest.", "values": {"passed": {"k": 3, "n": 3}}, "outcome": "positive"},
43
+ links=[{"rel": "produced", "from": r["id"]}])
44
+ lab = g.create(author="lab", type="run", inv=inv, fields={"kind": "lab", "name": "re-run of pytest", "state": "succeeded"},
45
+ links=[{"rel": "replicates", "to": r["id"], "data": {"differs": "same_code"}}])
46
+ g.create(author="lab", type="result", inv=inv, fields={"summary": "Re-executed: 1 of 1 values confirmed.", "values": {"passed": {"k": 3, "n": 3}}, "outcome": "positive"},
47
+ links=[{"rel": "produced", "from": lab["id"]}])
48
+ first = {p["p"]: p["met"] for p in g.check_work(o["order"])["predicates"]}
49
+ assert first == {"tests_pass": True, "claim_made": False}
50
+ rw = g.rework(author=H, order=o["order"], missing="no live claim on Q1 since the order was issued")
51
+ assert chain_root(g.snapshot(), rw["order"]) == o["order"]
52
+ second = {p["p"]: p["met"] for p in g.check_work(rw["order"])["predicates"]}
53
+ assert second["tests_pass"] is True # 0.4.0 lost it: evidence predated the rework
54
+ # (c) the claim of passing may now rest on the author's result: the instrument replicated its run positively
55
+ c = g.create(author="tester", type="claim", inv=inv, fields={"statement": "The importer passes all 3 of 3 tests in test_importer.py.",
56
+ "scope": "test_importer.py at this commit."},
57
+ links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs["id"]}])
58
+ assert c["code"].startswith("CL")
59
+ assert all(p["met"] for p in g.check_work(rw["order"])["predicates"])
60
+
61
+
62
+ def test_passing_still_needs_the_instrument():
63
+ g, inv, q = _g("software@1")
64
+ r = g.create(author="tester", type="run", inv=inv, fields={"kind": "agent", "name": "pytest", "state": "succeeded"})
65
+ rs = g.create(author="tester", type="result", inv=inv, fields={"summary": "Ran pytest.", "values": {"passed": {"k": 3, "n": 3}}, "outcome": "positive"},
66
+ links=[{"rel": "produced", "from": r["id"]}])
67
+ with pytest.raises(GraphError, match="PASS_WITHOUT_INSTRUMENT_RUN"):
68
+ g.create(author="tester", type="claim", inv=inv, fields={"statement": "The importer passes all 3 of 3 tests.", "scope": "test_importer.py at this commit."},
69
+ links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs["id"]}])
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