hypgraph 0.4.2__tar.gz → 0.4.3__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 (63) hide show
  1. {hypgraph-0.4.2 → hypgraph-0.4.3}/CHANGELOG.md +16 -0
  2. {hypgraph-0.4.2 → hypgraph-0.4.3}/PKG-INFO +1 -1
  3. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/__init__.py +1 -1
  4. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/checks.py +35 -4
  5. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/graph.py +33 -4
  6. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/schema.py +18 -0
  7. {hypgraph-0.4.2 → hypgraph-0.4.3}/tests/test_engine_fixes.py +5 -6
  8. {hypgraph-0.4.2 → hypgraph-0.4.3}/tests/test_refusals.py +2 -1
  9. hypgraph-0.4.3/tests/test_vocabulary_friction.py +130 -0
  10. {hypgraph-0.4.2 → hypgraph-0.4.3}/.gitignore +0 -0
  11. {hypgraph-0.4.2 → hypgraph-0.4.3}/AGENTS.md +0 -0
  12. {hypgraph-0.4.2 → hypgraph-0.4.3}/CITATION.cff +0 -0
  13. {hypgraph-0.4.2 → hypgraph-0.4.3}/LICENSE +0 -0
  14. {hypgraph-0.4.2 → hypgraph-0.4.3}/NOTICE +0 -0
  15. {hypgraph-0.4.2 → hypgraph-0.4.3}/README.md +0 -0
  16. {hypgraph-0.4.2 → hypgraph-0.4.3}/docs/PLAN.md +0 -0
  17. {hypgraph-0.4.2 → hypgraph-0.4.3}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
  18. {hypgraph-0.4.2 → hypgraph-0.4.3}/docs/decisions/ADR-002-governance-profiles.md +0 -0
  19. {hypgraph-0.4.2 → hypgraph-0.4.3}/docs/review-2026-10-24.md +0 -0
  20. {hypgraph-0.4.2 → hypgraph-0.4.3}/docs/spec.md +0 -0
  21. {hypgraph-0.4.2 → hypgraph-0.4.3}/pyproject.toml +0 -0
  22. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/__main__.py +0 -0
  23. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/bundle.py +0 -0
  24. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/cli.py +0 -0
  25. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/coord.py +0 -0
  26. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/demos/__init__.py +0 -0
  27. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/demos/broad_street.py +0 -0
  28. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/derive.py +0 -0
  29. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/language.py +0 -0
  30. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/mcp.py +0 -0
  31. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/plan.py +0 -0
  32. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/policy.py +0 -0
  33. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/profiles.py +0 -0
  34. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/refusals.py +0 -0
  35. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/report_check.py +0 -0
  36. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/server.py +0 -0
  37. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/skill/SKILL.md +0 -0
  38. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/stats.py +0 -0
  39. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/store.py +0 -0
  40. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/ui/__init__.py +0 -0
  41. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/ui/atlas.css +0 -0
  42. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/ui/atlas.js +0 -0
  43. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/ui/hypgraph.svg +0 -0
  44. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/ui/index.html +0 -0
  45. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
  46. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/ui/vendor/cose-base.js +0 -0
  47. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
  48. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
  49. {hypgraph-0.4.2 → hypgraph-0.4.3}/src/hypgraph/ui/vendor/layout-base.js +0 -0
  50. {hypgraph-0.4.2 → hypgraph-0.4.3}/studies/harmony/tests/test_suite.py +0 -0
  51. {hypgraph-0.4.2 → hypgraph-0.4.3}/tests/test_cancel_releases_lease.py +0 -0
  52. {hypgraph-0.4.2 → hypgraph-0.4.3}/tests/test_coord.py +0 -0
  53. {hypgraph-0.4.2 → hypgraph-0.4.3}/tests/test_d10.py +0 -0
  54. {hypgraph-0.4.2 → hypgraph-0.4.3}/tests/test_engine.py +0 -0
  55. {hypgraph-0.4.2 → hypgraph-0.4.3}/tests/test_incremental.py +0 -0
  56. {hypgraph-0.4.2 → hypgraph-0.4.3}/tests/test_log_and_check.py +0 -0
  57. {hypgraph-0.4.2 → hypgraph-0.4.3}/tests/test_order_detail.py +0 -0
  58. {hypgraph-0.4.2 → hypgraph-0.4.3}/tests/test_profiles.py +0 -0
  59. {hypgraph-0.4.2 → hypgraph-0.4.3}/tests/test_profiles_041.py +0 -0
  60. {hypgraph-0.4.2 → hypgraph-0.4.3}/tests/test_questions_not_assertions.py +0 -0
  61. {hypgraph-0.4.2 → hypgraph-0.4.3}/tests/test_server_auth.py +0 -0
  62. {hypgraph-0.4.2 → hypgraph-0.4.3}/tests/test_stats_values.py +0 -0
  63. {hypgraph-0.4.2 → hypgraph-0.4.3}/tests/test_surfaces.py +0 -0
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.3 — 2026-10-26
4
+
5
+ Five vocabulary-friction fixes from the HypSwarm discovery study, where 110 of 118 refusals were binding, naming
6
+ and shape errors rather than overclaims — and one correct answer was refused fifteen times.
7
+ - **A named p-value backs "significantly".** A bound field named `p`, `p_value` or `p_<test>` (an exact McNemar
8
+ `p_exact`), or such a field on a supporting result, satisfies the significance check; below 0.05 the word is
9
+ accepted, above it `SIGNIFICANCE_NOT_SHOWN` quotes it. The record's own two-proportion test stays the fallback.
10
+ - **A bound measurement's result supports the claim.** A claim written with `measurements` that name a result gets
11
+ a `supports` edge from that result, so its standing counts the line of support the writer already bound.
12
+ - **A wrong relation names the right one.** `executes can't join run → question … — a run joins a question with
13
+ part_of, motivated_by; the other way round, a question joins a run with …`.
14
+ - **Enum synonyms** (`schema.SYNONYMS`): `outcome=success` → `positive`, `kind=fact` → `finding`,
15
+ `state=done` → `succeeded`, and more; the stored value is canonical.
16
+ - **Small integers bind to a value the sentence names.** "produced 0 trades" binds to `oos_trades`,
17
+ "capacity is 7" to `capacity_total`; an unnamed small integer still binds only to a count.
18
+
3
19
  ## 0.4.2 — 2026-10-25
4
20
 
5
21
  - **A question is not an assertion.** `language.interrogative` / `asserted` return nothing for a sentence that ends
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hypgraph
3
- Version: 0.4.2
3
+ Version: 0.4.3
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.2"
19
+ __version__ = "0.4.3"
20
20
 
21
21
  from .bundle import verify_bundle # noqa: E402
22
22
  from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
@@ -274,6 +274,29 @@ def _measured_claim(s: Snap, support: list[str]) -> bool:
274
274
  return False
275
275
 
276
276
 
277
+ _PNAME = re.compile(r"^(p|pval|pvalue|p[_-]?val(?:ue)?|p[_-][a-z0-9_]+)$", re.I)
278
+
279
+
280
+ def _p_values(s: Snap, bound: list, support: list[str]) -> list[float]:
281
+ """The p-values behind a claim: a bound value dict's `p`, a bound field *named* p / p_value / p_<test>
282
+ (an exact McNemar p registered as `p_exact` is a test), and such fields on the results that support it."""
283
+ ps = [float(rv["p"]) for _m, rv in bound if isinstance(rv.get("p"), (int, float))]
284
+ for m, rv in bound:
285
+ key = str(m.get("key") or m.get("name") or "")
286
+ v = rv.get("value")
287
+ if _PNAME.match(key) and isinstance(v, (int, float)) and not isinstance(v, bool) and 0 <= v <= 1:
288
+ ps.append(float(v))
289
+ for rid in support:
290
+ n = s.nodes.get(rid)
291
+ if not n or n["type"] != "result":
292
+ continue
293
+ for key, rv in (n["body"].get("values") or {}).items():
294
+ v = rv.get("value") if isinstance(rv, dict) else rv
295
+ if _PNAME.match(str(key)) and isinstance(v, (int, float)) and not isinstance(v, bool) and 0 <= v <= 1:
296
+ ps.append(float(v))
297
+ return ps
298
+
299
+
277
300
  def _comparisons(s: Snap, stmt: str, bound: list, support: list[str]) -> list[Problem]:
278
301
  out = []
279
302
  comp = L.asserted(L.COMPARATIVE, stmt)
@@ -285,11 +308,13 @@ def _comparisons(s: Snap, stmt: str, bound: list, support: list[str]) -> list[Pr
285
308
  "(a k/n or value for both), or a difference with its interval",
286
309
  fix="bind both sides as measurements", words=comp))
287
310
  sig = L.asserted(L.SIGNIFICANCE, stmt)
288
- bound_p = [float(rv["p"]) for _m, rv in bound if isinstance(rv.get("p"), (int, float))]
311
+ bound_p = _p_values(s, bound, []) # the claim's own measurements
312
+ support_p = _p_values(s, [], support) # a named p on a supporting result: the last resort
289
313
  arms = [_arm(rv) for _m, rv in bound if _arm(rv)]
290
314
  no_sig = L.denied(L.SIGNIFICANCE, stmt)
291
315
  if no_sig and not sig: # "not significant": the record checks that too
292
- p = min(bound_p) if bound_p else (stats.two_proportions(*arms[0], *arms[1])["p"] if len(arms) >= 2 else None)
316
+ p = min(bound_p) if bound_p else (stats.two_proportions(*arms[0], *arms[1])["p"] if len(arms) >= 2
317
+ else (min(support_p) if support_p else None))
293
318
  if p is not None and p < 0.05:
294
319
  out.append(Problem("SIGNIFICANCE_CONTRADICTED",
295
320
  f"the statement denies significance ('{no_sig[0]}'), but the bound evidence is "
@@ -310,10 +335,16 @@ def _comparisons(s: Snap, stmt: str, bound: list, support: list[str]) -> list[Pr
310
335
  f"{t['diff'] * 100:.1f} points, 95% interval {t['lo'] * 100:.1f} to "
311
336
  f"{t['hi'] * 100:.1f}, p = {t['p']:.2g} (two-proportion z-test) — not significant "
312
337
  "at 0.05. Say the difference and that it isn't significant.", words=sig))
338
+ elif support_p:
339
+ if min(support_p) > 0.05:
340
+ out.append(Problem("SIGNIFICANCE_NOT_SHOWN",
341
+ f"'{sig[0]}': the p-value on the supporting result is {min(support_p):.3g}, not below "
342
+ "0.05 — say the difference and that it isn't significant", words=sig))
313
343
  else:
314
344
  out.append(Problem("SIGNIFICANCE_WITHOUT_TEST",
315
- f"'{sig[0]}' claims a test was passed: bind a result field holding the p-value or "
316
- "interval, or bind both sides as counts (k/n) so the record can test them",
345
+ f"'{sig[0]}' claims a test was passed: bind a result field holding the p-value (a field "
346
+ "named p, p_value or p_<test>, e.g. p_mcnemar) or an interval, or bind both sides as "
347
+ "counts (k/n) so the record can run its own two-proportion test",
317
348
  fix="bind a p-value, or both sides as k/n", words=sig))
318
349
  return out
319
350
 
@@ -31,7 +31,7 @@ from . import derive as D
31
31
  from . import refusals, store
32
32
  from .checks import claim_issues
33
33
  from .policy import Policy
34
- from .schema import REPLICATION, RELS, SAME_TYPE, TYPES, VERDICTS
34
+ from .schema import REPLICATION, RELS, SAME_TYPE, SYNONYMS, TYPES, VERDICTS
35
35
  from .store import canon, now
36
36
 
37
37
 
@@ -69,6 +69,7 @@ def clean_body(ntype: str, body: dict, *, partial_ok: bool = False) -> dict:
69
69
  if req and len(v) < (3 if kind == "str" else 10):
70
70
  raise GraphError(f"{k}: say it in a full sentence")
71
71
  elif kind.startswith("enum:"):
72
+ v = SYNONYMS.get((ntype, k), {}).get(str(v).strip().lower().replace(" ", "_").replace("-", "_"), str(v).strip())
72
73
  if str(v) not in kind[5:].split("|"):
73
74
  raise GraphError(f"{k} must be one of {kind[5:].replace('|', ', ')}")
74
75
  v = str(v)
@@ -406,8 +407,12 @@ class Graph:
406
407
  st, dt = s.nodes[src]["type"], s.nodes[dst]["type"]
407
408
  srcs, dsts = RELS[rel]
408
409
  if st not in srcs or dt not in dsts:
410
+ joins = [r for r, (a, b) in RELS.items() if st in a and dt in b and r not in SAME_TYPE]
411
+ back = [r for r, (a, b) in RELS.items() if dt in a and st in b and r not in SAME_TYPE and r not in joins]
412
+ hint = (f" — a {st} joins a {dt} with {', '.join(joins)}" if joins else "") + \
413
+ (f"; the other way round, a {dt} joins a {st} with {', '.join(back)}" if back else "")
409
414
  raise GraphError(f"{rel} can't join {st} → {dt} (allowed: {', '.join(sorted(srcs))} → "
410
- f"{', '.join(sorted(dsts))})")
415
+ f"{', '.join(sorted(dsts))}){hint}")
411
416
  if rel in SAME_TYPE and st != dt:
412
417
  raise GraphError(f"{rel} joins two nodes of the same type")
413
418
  if src == dst:
@@ -577,6 +582,19 @@ class Graph:
577
582
  if type == "claim":
578
583
  body = self._bind_measurements(s, body, inv)
579
584
  staged["body"] = body
585
+ # a measurement names the result its number comes from: that result supports the claim, so the
586
+ # standing counts it as a line of support without a separate link from the writer
587
+ for m in body.get("measurements") or []:
588
+ rid = m.get("result") if isinstance(m, dict) else None
589
+ if rid not in s.nodes:
590
+ rid = next((i for i, n in s.nodes.items() if n.get("code") == rid and n["inv"] == inv), None)
591
+ if rid and s.nodes[rid]["type"] == "result" and rid != nid and not any(
592
+ (a == rid and b == nid and rel == "supports") or (a == nid and b == rid and rel == "derived_from")
593
+ for a, rel, b in pending):
594
+ a, rel, b, _f = self._orient(s, rid, "supports", nid)
595
+ self._check_link(s, a, rel, b)
596
+ pending.append((a, rel, b))
597
+ links.append({"rel": "supports", "from": rid, "why": "a measurement of this claim is this result's"})
580
598
  if type == "claim" and author not in P.curators:
581
599
  problems = claim_issues(s, body, self._support_ids(s, nid, pending), inv)
582
600
  loose = [w for p in problems if p.code == "UNBOUND_NUMBER" for w in p.words]
@@ -658,9 +676,17 @@ class Graph:
658
676
  @staticmethod
659
677
  def _auto_bind(s: D.Snap, body: dict, inv: str, author: str, support: list[str], loose: list[str]) -> tuple:
660
678
  """Bind the statement's loose numbers to results the claim already links, then to the author's own results
661
- in this investigation (newest first). Small integers (≤ 12) bind only to counts (k, n), never to a value, so
662
- a limit or a day of the month can't borrow a field by coincidence. Returns (body, results to link, notes)."""
679
+ in this investigation (newest first). Small integers (≤ 12) bind only to counts (k, n) — or to a value whose
680
+ field the sentence names ("0 trades" to `oos_trades`, "capacity is 7" to `capacity_total`) — never to an
681
+ unnamed value, so a limit or a day of the month can't borrow a field by coincidence.
682
+ Returns (body, results to link, notes)."""
683
+ import re as _re
663
684
  from .checks import _arm, _matches, _nums, numbers
685
+
686
+ def named(key: str, text: str) -> bool:
687
+ words = [w for w in _re.split(r"[^a-z0-9]+", key.lower()) if len(w) >= 3]
688
+ low = text.lower()
689
+ return bool(words) and any(_re.search(r"\b" + _re.escape(w), low) for w in words)
664
690
  stmt = body.get("statement", "")
665
691
  linked = [x for x in support if (s.nodes.get(x) or {}).get("type") == "result"]
666
692
  mine = sorted((i for i, n in s.nodes.items() if n["type"] == "result" and n["inv"] == inv
@@ -686,6 +712,9 @@ class Graph:
686
712
  arm = _arm(rv)
687
713
  vals = ([float(arm[0]), float(arm[1])] if arm else []) if small else _nums(rv) + \
688
714
  ([arm[0] / arm[1]] if arm else [])
715
+ if small and not arm and isinstance(rv.get("value"), (int, float)) and not isinstance(rv.get("value"), bool) \
716
+ and named(key, stmt):
717
+ vals = [float(rv["value"])]
689
718
  if vals and _matches(printed, vals):
690
719
  m = {"name": key, "result": rid, "key": key}
691
720
  if arm:
@@ -101,6 +101,24 @@ RELS: dict[str, tuple[set, set]] = {
101
101
  "settles": ({"decision"}, {"task"}),
102
102
  }
103
103
  REPLICATION = ("same_code", "new_seed", "new_impl", "new_data", "new_env")
104
+ # What writers call the enum values. Mapped before validation, by (type, field); the stored value is canonical.
105
+ SYNONYMS: dict[tuple[str, str], dict[str, str]] = {
106
+ ("result", "outcome"): {"success": "positive", "succeeded": "positive", "successful": "positive", "pass": "positive",
107
+ "passed": "positive", "ok": "positive", "yes": "positive", "confirmed": "positive",
108
+ "supported": "positive", "no": "negative", "refuted": "negative", "disconfirmed": "negative",
109
+ "null": "negative", "unclear": "inconclusive", "mixed": "inconclusive",
110
+ "indeterminate": "inconclusive", "error": "failed", "crashed": "failed", "fail": "failed",
111
+ "failure": "failed", "skip": "skipped", "not_run": "skipped"},
112
+ ("result", "kind"): {"calculated": "computed", "derived": "computed", "measured": "observed", "empirical": "observed",
113
+ "quoted": "cited", "literature": "cited"},
114
+ ("claim", "kind"): {"fact": "finding", "result": "finding", "observation": "finding", "measurement": "finding",
115
+ "conclusion": "finding", "rule_out": "exclusion", "ruled_out": "exclusion", "negative": "exclusion",
116
+ "undetermined": "inconclusive", "unclear": "inconclusive", "definition": "convention",
117
+ "notation": "convention", "hunch": "lead", "idea": "lead"},
118
+ ("run", "state"): {"success": "succeeded", "successful": "succeeded", "done": "succeeded", "completed": "succeeded",
119
+ "complete": "succeeded", "ok": "succeeded", "error": "failed", "failure": "failed", "fail": "failed",
120
+ "crashed": "failed", "timed_out": "timeout", "killed": "cancelled", "stopped": "interrupted"},
121
+ }
104
122
  # what a node rests on: if the far end falls, this node is at risk (a work order is moot)
105
123
  RISKY = ("requires", "derived_from", "based_on", "works_on", "rests_on")
106
124
  SAME_TYPE = ("supersedes", "corrects")
@@ -101,15 +101,14 @@ def test_binding_prefers_linked_results_and_only_the_authors_own(env):
101
101
  assert other
102
102
 
103
103
 
104
- def test_small_integers_bind_only_to_counts(env):
104
+ def test_small_integers_bind_only_to_counts_or_a_named_value(env):
105
105
  g, me, inv, r = env
106
106
  me.add("result", inv, summary="Seven of nine probes leaked.", values={"leaks": {"k": 7, "n": 9}, "limit": {"value": 5}})
107
- with pytest.raises(GraphError) as e: # 5 is not bound to the value field 'limit'
108
- me.add("claim", inv, statement="7 of 9 probes leaked, against a limit of 5 mg/L.", scope=SCOPE)
107
+ with pytest.raises(GraphError) as e: # 5 is not bound to 'limit' when the sentence doesn't name it
108
+ me.add("claim", inv, statement="7 of 9 probes leaked, 5 of them twice.", scope=SCOPE)
109
109
  assert e.value.problems[0]["words"] == ["5"]
110
- n = me.add("claim", inv, statement="7 of 9 probes leaked, against a limit of 5 mg/L.",
111
- scope=SCOPE + " The limit is 5 mg/L.") # a design constant belongs in the scope
112
- assert {m["key"] for m in n["body"]["measurements"]} == {"leaks"}
110
+ n = me.add("claim", inv, statement="7 of 9 probes leaked, against a limit of 5 mg/L.", scope=SCOPE)
111
+ assert {m["key"] for m in n["body"]["measurements"]} == {"leaks", "limit"} # 0.4.3: the sentence names the field
113
112
 
114
113
 
115
114
  def test_revise_binds_too(env):
@@ -128,7 +128,8 @@ def test_significance_is_tested_from_the_counts(env):
128
128
  p = refused(env, "Model A is significantly better than model B.", [M(env[3], "p", "p_ab", value=0.21)] + close,
129
129
  scope="1,000 items each, one seed.")
130
130
  assert codes(p) == ["SIGNIFICANCE_NOT_SHOWN"]
131
- assert "SIGNIFICANCE_WITHOUT_TEST" in codes(refused(env, "The fine-tune is significantly better."))
131
+ # 0.4.3: the supporting result carries p_ab = 0.21, a named p-value — that test backs the word and fails it
132
+ assert "SIGNIFICANCE_NOT_SHOWN" in codes(refused(env, "The fine-tune is significantly better."))
132
133
 
133
134
 
134
135
  def test_causes_need_an_intervention(env):
@@ -0,0 +1,130 @@
1
+ """0.4.3: the five vocabulary-friction fixes from the discovery study (HypSwarm studies/discovery/LIB-NOTES.md
2
+ items 1, 2, 4, 5, 10). Each test is the sentence a real model wrote and was refused for."""
3
+ import pytest
4
+
5
+ from hypgraph import Graph, GraphError, Policy
6
+
7
+ H = "harness"
8
+
9
+
10
+ def _g():
11
+ g = Graph(policy=Policy(reviewers={"critic"}, extra_reserved={H}))
12
+ inv = g.create(author=H, type="investigation", fields={"title": "Does A beat B.", "objective": "Compare A and B on the same items."})["id"]
13
+ q = g.create(author=H, type="question", inv=inv, code="Q2", fields={"text": "Is A significantly more accurate than B?"})["id"]
14
+ return g, inv, q
15
+
16
+
17
+ def _result(g, inv, author, values, summary="Measured what the question asks."):
18
+ r = g.create(author=author, type="run", inv=inv, fields={"kind": "agent", "name": "a run", "state": "succeeded"})
19
+ return g.create(author=author, type="result", inv=inv, fields={"summary": summary, "values": values},
20
+ links=[{"rel": "produced", "from": r["id"]}])["id"]
21
+
22
+
23
+ # ── item 1: a named p-value is a test ─────────────────────────────────────────
24
+ def test_a_named_p_value_backs_significantly():
25
+ g, inv, q = _g()
26
+ rs = _result(g, inv, "stats", {"acc_a": {"value": 0.837}, "acc_b": {"value": 0.796}, "p_exact": {"value": 0.0169}},
27
+ "Exact McNemar test on the paired items.")
28
+ c = g.create(author="stats", type="claim", inv=inv,
29
+ fields={"statement": "A is significantly more accurate than B (0.837 vs 0.796, exact McNemar p = 0.0169).",
30
+ "scope": "The 1,000 paired items; two-sided test at 0.05."},
31
+ links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs}])
32
+ assert c["code"].startswith("CL")
33
+
34
+
35
+ def test_a_named_p_value_above_the_threshold_is_quoted_back():
36
+ g, inv, q = _g()
37
+ rs = _result(g, inv, "stats", {"acc_a": {"value": 0.805}, "acc_b": {"value": 0.781}, "p_mcnemar": {"value": 0.208}})
38
+ with pytest.raises(GraphError, match="SIGNIFICANCE_NOT_SHOWN"):
39
+ g.create(author="stats", type="claim", inv=inv,
40
+ fields={"statement": "A is significantly more accurate than B (0.805 vs 0.781, p = 0.208).",
41
+ "scope": "The 1,000 paired items; two-sided test at 0.05."},
42
+ links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs}])
43
+
44
+
45
+ def test_a_supporting_results_p_field_counts_even_when_the_sentence_omits_it():
46
+ g, inv, q = _g()
47
+ rs = _result(g, inv, "stats", {"acc_a": {"value": 0.837}, "acc_b": {"value": 0.796}, "p_value": {"value": 0.017}})
48
+ c = g.create(author="stats", type="claim", inv=inv,
49
+ fields={"statement": "A is significantly more accurate than B (0.837 against 0.796).",
50
+ "scope": "The 1,000 paired items; exact McNemar, two-sided at 0.05."},
51
+ links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs}])
52
+ assert c["code"].startswith("CL")
53
+
54
+
55
+ def test_significantly_with_no_test_anywhere_is_still_refused():
56
+ g, inv, q = _g()
57
+ rs = _result(g, inv, "stats", {"acc_a": {"value": 0.837}, "acc_b": {"value": 0.796}})
58
+ with pytest.raises(GraphError, match="SIGNIFICANCE_WITHOUT_TEST"):
59
+ g.create(author="stats", type="claim", inv=inv,
60
+ fields={"statement": "A is significantly more accurate than B (0.837 against 0.796).", "scope": "The 1,000 paired items."},
61
+ links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs}])
62
+
63
+
64
+ # ── item 2: a bound measurement's result supports the claim ───────────────────
65
+ def test_a_measurement_counts_as_a_line_of_support():
66
+ g, inv, q = _g()
67
+ rs = _result(g, inv, "analyst", {"acc_a": {"k": 805, "n": 1000}})
68
+ c = g.create(author="analyst", type="claim", inv=inv,
69
+ fields={"statement": "A is right on 805 of 1000 items.", "scope": "The 1,000 items in items.csv.",
70
+ "measurements": [{"name": "acc_a", "result": rs, "key": "acc_a", "k": 805, "n": 1000}]},
71
+ links=[{"rel": "answers", "to": q}])
72
+ st = g.get(c["id"])["state"]
73
+ assert "1 independent line of support" in st["sentence"], st["sentence"]
74
+ assert any(e["rel"] == "supports" and e["src"] == rs for e in g.get(c["id"])["in"])
75
+
76
+
77
+ # ── item 4: a wrong relation names the right one ──────────────────────────────
78
+ def test_a_wrong_relation_names_the_ones_that_join():
79
+ g, inv, q = _g()
80
+ r = g.create(author="w", type="run", inv=inv, fields={"kind": "agent", "name": "pytest", "state": "succeeded"})
81
+ with pytest.raises(GraphError) as e:
82
+ g.create(author="w", type="result", inv=inv, fields={"summary": "Ran the tests.", "values": {"passed": {"k": 4, "n": 5}}},
83
+ links=[{"rel": "executes", "from": r["id"]}]) # a run "executes" the result it produced: wrong relation
84
+ msg = str(e.value)
85
+ assert "executes can't join run → result" in msg and "a run joins a result with produced" in msg and "a result joins a run with derived_from" in msg
86
+
87
+
88
+ # ── item 5: enum synonyms ─────────────────────────────────────────────────────
89
+ def test_enum_synonyms_are_stored_canonically():
90
+ g, inv, q = _g()
91
+ r = g.create(author="w", type="run", inv=inv, fields={"kind": "agent", "name": "pytest", "state": "done"})
92
+ assert g.get(r["id"])["body"]["state"] == "succeeded"
93
+ rs = g.create(author="w", type="result", inv=inv, fields={"summary": "Ran the tests.", "outcome": "success", "kind": "calculated",
94
+ "values": {"passed": {"k": 5, "n": 5}}},
95
+ links=[{"rel": "produced", "from": r["id"]}])
96
+ assert g.get(rs["id"])["body"]["outcome"] == "positive" and g.get(rs["id"])["body"]["kind"] == "computed"
97
+ c = g.create(author="w", type="claim", inv=inv, fields={"statement": "All 5 of 5 tests pass in tests/test_money.py.",
98
+ "scope": "tests/test_money.py after the fix.", "kind": "fact"},
99
+ links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs["id"]}])
100
+ assert g.get(c["id"])["body"]["kind"] == "finding"
101
+ with pytest.raises(GraphError, match="must be one of"):
102
+ g.create(author="w", type="result", inv=inv, fields={"summary": "Ran the tests.", "outcome": "splendid"},
103
+ links=[{"rel": "produced", "from": r["id"]}])
104
+
105
+
106
+ # ── item 10: small integers bind to a value the sentence names ───────────────
107
+ def test_a_small_integer_binds_to_the_field_the_sentence_names():
108
+ g, inv, q = _g()
109
+ rs = _result(g, inv, "quant", {"oos_trades": {"value": 0}, "fees": {"value": 0.001}, "slippage": {"value": 0.0005}})
110
+ c = g.create(author="quant", type="claim", inv=inv,
111
+ fields={"statement": "The held-out window produced 0 trades after costs, too few to say anything about an edge.",
112
+ "scope": "Days 600-999 of prices.csv; 10 bp fees and 5 bp slippage per side."},
113
+ links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs}])
114
+ assert any(m["key"] == "oos_trades" for m in g.get(c["id"])["body"]["measurements"])
115
+ rs2 = _result(g, inv, "solver", {"capacity_total": {"value": 7}, "customers": {"value": 8}, "feasible": {"value": False}})
116
+ c2 = g.create(author="solver", type="claim", inv=inv,
117
+ fields={"statement": "No feasible assignment exists: total capacity is 7 for 8 customers.",
118
+ "scope": "instance.csv as given; depots d0, d1, d2."}, # the scope names no numbers: both must bind
119
+ links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs2}])
120
+ keys = {m["key"] for m in g.get(c2["id"])["body"]["measurements"]}
121
+ assert {"capacity_total", "customers"} <= keys
122
+
123
+
124
+ def test_an_unnamed_small_integer_still_does_not_borrow_a_value():
125
+ g, inv, q = _g()
126
+ rs = _result(g, inv, "quant", {"oos_trades": {"value": 7}})
127
+ with pytest.raises(GraphError, match="UNBOUND_NUMBER"):
128
+ g.create(author="quant", type="claim", inv=inv,
129
+ fields={"statement": "The strategy wins 7 points.", "scope": "Days 600-999 of prices.csv."},
130
+ links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs}])
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