hypgraph 0.4.1__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.
- {hypgraph-0.4.1 → hypgraph-0.4.3}/CHANGELOG.md +24 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/PKG-INFO +1 -1
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/__init__.py +1 -1
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/checks.py +35 -4
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/graph.py +33 -4
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/language.py +13 -1
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/report_check.py +11 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/schema.py +18 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/tests/test_engine_fixes.py +5 -6
- hypgraph-0.4.3/tests/test_questions_not_assertions.py +43 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/tests/test_refusals.py +2 -1
- hypgraph-0.4.3/tests/test_vocabulary_friction.py +130 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/.gitignore +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/AGENTS.md +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/CITATION.cff +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/LICENSE +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/NOTICE +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/README.md +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/docs/PLAN.md +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/docs/decisions/ADR-002-governance-profiles.md +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/docs/review-2026-10-24.md +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/docs/spec.md +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/pyproject.toml +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/__main__.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/bundle.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/cli.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/coord.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/demos/__init__.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/demos/broad_street.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/derive.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/mcp.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/plan.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/policy.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/profiles.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/refusals.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/server.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/skill/SKILL.md +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/stats.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/store.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/ui/__init__.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/ui/atlas.css +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/ui/atlas.js +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/ui/hypgraph.svg +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/ui/index.html +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/ui/vendor/cose-base.js +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/src/hypgraph/ui/vendor/layout-base.js +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/studies/harmony/tests/test_suite.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/tests/test_cancel_releases_lease.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/tests/test_coord.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/tests/test_d10.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/tests/test_engine.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/tests/test_incremental.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/tests/test_log_and_check.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/tests/test_order_detail.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/tests/test_profiles.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/tests/test_profiles_041.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/tests/test_server_auth.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/tests/test_stats_values.py +0 -0
- {hypgraph-0.4.1 → hypgraph-0.4.3}/tests/test_surfaces.py +0 -0
|
@@ -1,5 +1,29 @@
|
|
|
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
|
+
|
|
19
|
+
## 0.4.2 — 2026-10-25
|
|
20
|
+
|
|
21
|
+
- **A question is not an assertion.** `language.interrogative` / `asserted` return nothing for a sentence that ends
|
|
22
|
+
with a question mark, and the report check skips the language and number rules on a sentence that asks or that
|
|
23
|
+
quotes one of the investigation's own questions (citations are still checked). Found in the first real HypSwarm
|
|
24
|
+
mixed-swarm run: the lead headed a section "Q2 — Is A significantly more accurate than B…?" and lost its report
|
|
25
|
+
to `REPORT_SIGNIFICANCE_UNBACKED` three times.
|
|
26
|
+
|
|
3
27
|
## 0.4.1 — 2026-10-25
|
|
4
28
|
|
|
5
29
|
Three rules the hypswarm examples exposed, each with a test:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hypgraph
|
|
3
|
-
Version: 0.4.
|
|
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.
|
|
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 =
|
|
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
|
|
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
|
|
316
|
-
"
|
|
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)
|
|
662
|
-
|
|
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:
|
|
@@ -64,9 +64,21 @@ _ZERO_ON_A_SCALE = re.compile(r"\b(below|above|near|to|from|of|than|at|cross(?:e
|
|
|
64
64
|
re.I)
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
# a sentence that asks: it ends with a question mark (markdown emphasis or a closing quote may follow it)
|
|
68
|
+
_INTERROGATIVE = re.compile(r"\?\s*[*_\"'’”)\]]*\s*$")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def interrogative(text: str) -> bool:
|
|
72
|
+
"""Does the sentence ask rather than assert? "Is A significantly better than B?" asserts nothing — found when a
|
|
73
|
+
lead headed a report section with the question it was answering and lost the report to the significance check."""
|
|
74
|
+
return bool(_INTERROGATIVE.search((text or "").strip()))
|
|
75
|
+
|
|
76
|
+
|
|
67
77
|
def asserted(pattern: re.Pattern, text: str) -> list[str]:
|
|
68
78
|
"""The distinct matches of a pattern that their sentence actually asserts: not negated in their clause, not an
|
|
69
|
-
epistemic hedge ("cannot conclude"),
|
|
79
|
+
epistemic hedge ("cannot conclude"), not a number on a scale ("below zero"), and nothing in a question."""
|
|
80
|
+
if interrogative(text):
|
|
81
|
+
return []
|
|
70
82
|
out = []
|
|
71
83
|
for m in pattern.finditer(text or ""):
|
|
72
84
|
if negated(text, m.start()):
|
|
@@ -71,6 +71,10 @@ _NEGATOR = re.compile(r"\b(not|cannot|can't|never|no|nor|without|hardly|unlikely
|
|
|
71
71
|
r"doesn't|didn't|won't)\b[\w\s-]{0,24}$", re.I)
|
|
72
72
|
|
|
73
73
|
|
|
74
|
+
def _norm(text: str) -> str:
|
|
75
|
+
return re.sub(r"[^a-z0-9]+", " ", (text or "").lower()).strip()
|
|
76
|
+
|
|
77
|
+
|
|
74
78
|
def _own_words(sentence: str) -> str:
|
|
75
79
|
"""The writer's own words: quoted passages (often the very wording being corrected) blanked out."""
|
|
76
80
|
return _QUOTE.sub(lambda m: " " * len(m.group(0)), sentence)
|
|
@@ -152,6 +156,10 @@ def check(g: "Graph", text: str, inv: str = "") -> dict:
|
|
|
152
156
|
if n["code"] and (n["inv"] in invs or i in invs):
|
|
153
157
|
by_code.setdefault(n["code"], []).append(i)
|
|
154
158
|
plain, stat = _allowed(s, invs)
|
|
159
|
+
# the investigations' own questions: a report may quote the question it answers without asserting it
|
|
160
|
+
qtexts = [_norm(n["body"].get("text", "")) for i, n in s.nodes.items()
|
|
161
|
+
if n["type"] == "question" and (n["inv"] in invs or i in invs)]
|
|
162
|
+
qtexts = [q for q in qtexts if len(q) >= 20]
|
|
155
163
|
findings: list[dict] = []
|
|
156
164
|
counted = {"sentences": 0, "numbers": 0, "codes": 0}
|
|
157
165
|
|
|
@@ -170,6 +178,7 @@ def check(g: "Graph", text: str, inv: str = "") -> dict:
|
|
|
170
178
|
continue
|
|
171
179
|
counted["sentences"] += 1
|
|
172
180
|
own = _own_words(sent)
|
|
181
|
+
asks = L.interrogative(sent) or any(q in _norm(sent) for q in qtexts)
|
|
173
182
|
qualified = bool(_QUALIFIED.search(sent))
|
|
174
183
|
settled = _asserted(_SETTLED, own)
|
|
175
184
|
cited = []
|
|
@@ -208,6 +217,8 @@ def check(g: "Graph", text: str, inv: str = "") -> dict:
|
|
|
208
217
|
add(ln, "warning", "REPORT_OMITS_DISPUTE", sent,
|
|
209
218
|
f"{code} is {'contested' if review == 'contested' else 'at risk'} in the record — say so where "
|
|
210
219
|
"you cite it.")
|
|
220
|
+
if asks: # a question, or the question's own words: citations checked, nothing asserted
|
|
221
|
+
continue
|
|
211
222
|
# numbers
|
|
212
223
|
for p in numbers(sent):
|
|
213
224
|
if _skip_number(p, sent) or re.search(rf"(?<![\w-])[A-Z]{{1,3}}{re.escape(p[3])}(?![\w-])", sent):
|
|
@@ -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
|
|
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
|
|
108
|
-
me.add("claim", inv, statement="7 of 9 probes leaked,
|
|
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
|
-
|
|
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):
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""0.4.2: a question is not an assertion. From the first real HypSwarm mixed-swarm run: the lead headed a report
|
|
2
|
+
section with the question it was answering and the report was refused three times for 'significantly'."""
|
|
3
|
+
from hypgraph import Graph, Policy
|
|
4
|
+
from hypgraph import language as L
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_interrogatives_assert_nothing():
|
|
8
|
+
assert L.asserted(L.SIGNIFICANCE, "Is A significantly more accurate than B?") == []
|
|
9
|
+
assert L.asserted(L.SIGNIFICANCE, "**Q2 — Is A significantly more accurate than B at the two-sided 0.05 level?**") == []
|
|
10
|
+
assert L.asserted(L.ABSOLUTE, "Does it never fail?") == []
|
|
11
|
+
assert L.asserted(L.SIGNIFICANCE, "A is significantly more accurate than B.") == ["significantly"]
|
|
12
|
+
assert L.interrogative("Is it so?\"") and not L.interrogative("It is so.")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _record():
|
|
16
|
+
g = Graph(policy=Policy(reviewers={"critic"}))
|
|
17
|
+
inv = g.create(author="h", type="investigation", fields={"title": "Does A beat B.", "objective": "Compare A and B on the same items."})["id"]
|
|
18
|
+
q = g.create(author="h", type="question", inv=inv, code="Q2",
|
|
19
|
+
fields={"text": "Is A significantly more accurate than B (two-sided, 0.05)?"})["id"]
|
|
20
|
+
r = g.create(author="stats", type="run", inv=inv, fields={"kind": "agent", "name": "mcnemar", "state": "succeeded"})
|
|
21
|
+
rs = g.create(author="stats", type="result", inv=inv, fields={"summary": "Compared A and B on the same 1,000 items.",
|
|
22
|
+
"values": {"a": {"k": 900, "n": 1000}, "b": {"k": 800, "n": 1000}}},
|
|
23
|
+
links=[{"rel": "produced", "from": r["id"]}])
|
|
24
|
+
c = g.create(author="stats", type="claim", inv=inv, fields={"statement": "A is right on 900 of 1000 items and B on 800 of 1000.",
|
|
25
|
+
"scope": "The 1,000 shared items."},
|
|
26
|
+
links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs["id"]}])
|
|
27
|
+
g.review(author="critic", target=c["id"], verdict="checked", method="Recounted both columns from the file.", notes="The counts hold and the claim says what the result says.")
|
|
28
|
+
return g, inv, c["code"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_a_report_may_quote_the_question_it_answers():
|
|
32
|
+
g, inv, cl = _record()
|
|
33
|
+
text = ("## Q2 — Is A significantly more accurate than B at the two-sided 0.05 level?\n\n"
|
|
34
|
+
f"A is right on 900 of 1000 items and B on 800 of 1000 ({cl}).\n\n"
|
|
35
|
+
"The question 'Is A significantly more accurate than B (two-sided, 0.05)?' is what this section answers.\n")
|
|
36
|
+
r = g.check_text(text, inv)
|
|
37
|
+
assert not [f for f in r["findings"] if f["code"] == "REPORT_SIGNIFICANCE_UNBACKED"], r["findings"]
|
|
38
|
+
assert r["ok"]
|
|
39
|
+
# the assertion itself is still caught, and an unknown code in a question heading is still an unknown code
|
|
40
|
+
bad = g.check_text("A is significantly more accurate than B.", inv)
|
|
41
|
+
assert any(f["code"] == "REPORT_SIGNIFICANCE_UNBACKED" and f["level"] == "error" for f in bad["findings"])
|
|
42
|
+
unk = g.check_text("## Is CL99 significantly better?", inv)
|
|
43
|
+
assert any(f["code"] == "REPORT_UNKNOWN_CODE" for f in unk["findings"])
|
|
@@ -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
|
-
|
|
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
|
|
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
|