hypgraph 0.4.0__tar.gz → 0.4.2__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 (62) hide show
  1. {hypgraph-0.4.0 → hypgraph-0.4.2}/CHANGELOG.md +18 -0
  2. {hypgraph-0.4.0 → hypgraph-0.4.2}/PKG-INFO +1 -1
  3. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/__init__.py +1 -1
  4. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/checks.py +3 -0
  5. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/cli.py +1 -1
  6. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/coord.py +11 -1
  7. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/language.py +13 -1
  8. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/profiles.py +20 -2
  9. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/report_check.py +11 -0
  10. hypgraph-0.4.2/tests/test_profiles_041.py +69 -0
  11. hypgraph-0.4.2/tests/test_questions_not_assertions.py +43 -0
  12. {hypgraph-0.4.0 → hypgraph-0.4.2}/.gitignore +0 -0
  13. {hypgraph-0.4.0 → hypgraph-0.4.2}/AGENTS.md +0 -0
  14. {hypgraph-0.4.0 → hypgraph-0.4.2}/CITATION.cff +0 -0
  15. {hypgraph-0.4.0 → hypgraph-0.4.2}/LICENSE +0 -0
  16. {hypgraph-0.4.0 → hypgraph-0.4.2}/NOTICE +0 -0
  17. {hypgraph-0.4.0 → hypgraph-0.4.2}/README.md +0 -0
  18. {hypgraph-0.4.0 → hypgraph-0.4.2}/docs/PLAN.md +0 -0
  19. {hypgraph-0.4.0 → hypgraph-0.4.2}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
  20. {hypgraph-0.4.0 → hypgraph-0.4.2}/docs/decisions/ADR-002-governance-profiles.md +0 -0
  21. {hypgraph-0.4.0 → hypgraph-0.4.2}/docs/review-2026-10-24.md +0 -0
  22. {hypgraph-0.4.0 → hypgraph-0.4.2}/docs/spec.md +0 -0
  23. {hypgraph-0.4.0 → hypgraph-0.4.2}/pyproject.toml +0 -0
  24. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/__main__.py +0 -0
  25. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/bundle.py +0 -0
  26. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/demos/__init__.py +0 -0
  27. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/demos/broad_street.py +0 -0
  28. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/derive.py +0 -0
  29. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/graph.py +0 -0
  30. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/mcp.py +0 -0
  31. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/plan.py +0 -0
  32. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/policy.py +0 -0
  33. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/refusals.py +0 -0
  34. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/schema.py +0 -0
  35. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/server.py +0 -0
  36. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/skill/SKILL.md +0 -0
  37. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/stats.py +0 -0
  38. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/store.py +0 -0
  39. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/ui/__init__.py +0 -0
  40. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/ui/atlas.css +0 -0
  41. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/ui/atlas.js +0 -0
  42. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/ui/hypgraph.svg +0 -0
  43. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/ui/index.html +0 -0
  44. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
  45. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/ui/vendor/cose-base.js +0 -0
  46. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
  47. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
  48. {hypgraph-0.4.0 → hypgraph-0.4.2}/src/hypgraph/ui/vendor/layout-base.js +0 -0
  49. {hypgraph-0.4.0 → hypgraph-0.4.2}/studies/harmony/tests/test_suite.py +0 -0
  50. {hypgraph-0.4.0 → hypgraph-0.4.2}/tests/test_cancel_releases_lease.py +0 -0
  51. {hypgraph-0.4.0 → hypgraph-0.4.2}/tests/test_coord.py +0 -0
  52. {hypgraph-0.4.0 → hypgraph-0.4.2}/tests/test_d10.py +0 -0
  53. {hypgraph-0.4.0 → hypgraph-0.4.2}/tests/test_engine.py +0 -0
  54. {hypgraph-0.4.0 → hypgraph-0.4.2}/tests/test_engine_fixes.py +0 -0
  55. {hypgraph-0.4.0 → hypgraph-0.4.2}/tests/test_incremental.py +0 -0
  56. {hypgraph-0.4.0 → hypgraph-0.4.2}/tests/test_log_and_check.py +0 -0
  57. {hypgraph-0.4.0 → hypgraph-0.4.2}/tests/test_order_detail.py +0 -0
  58. {hypgraph-0.4.0 → hypgraph-0.4.2}/tests/test_profiles.py +0 -0
  59. {hypgraph-0.4.0 → hypgraph-0.4.2}/tests/test_refusals.py +0 -0
  60. {hypgraph-0.4.0 → hypgraph-0.4.2}/tests/test_server_auth.py +0 -0
  61. {hypgraph-0.4.0 → hypgraph-0.4.2}/tests/test_stats_values.py +0 -0
  62. {hypgraph-0.4.0 → hypgraph-0.4.2}/tests/test_surfaces.py +0 -0
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.2 — 2026-10-25
4
+
5
+ - **A question is not an assertion.** `language.interrogative` / `asserted` return nothing for a sentence that ends
6
+ with a question mark, and the report check skips the language and number rules on a sentence that asks or that
7
+ quotes one of the investigation's own questions (citations are still checked). Found in the first real HypSwarm
8
+ mixed-swarm run: the lead headed a section "Q2 — Is A significantly more accurate than B…?" and lost its report
9
+ to `REPORT_SIGNIFICANCE_UNBACKED` three times.
10
+
11
+ ## 0.4.1 — 2026-10-25
12
+
13
+ Three rules the hypswarm examples exposed, each with a test:
14
+ - A claim's numbers that appear in a cited source's passage, citation or title are accounted for (a knowledge claim
15
+ "signed on 24 October 1648" citing the passage that says so is no longer `UNBOUND_NUMBER`).
16
+ - A rework's predicates count evidence written since the **first** order of its chain (`coord.chain_root`): a
17
+ `tests_pass` met by the instrument's re-run under the original order stays met under the rework.
18
+ - `PASS_WITHOUT_INSTRUMENT_RUN` accepts a claim resting on the author's result when a live lab run `replicates`
19
+ the run that produced it and the re-run's result is positive (`profiles._instrument_backed`).
20
+
3
21
  ## 0.4.0 — 2026-10-25: the coordination layer
4
22
 
5
23
  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.2
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.2"
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"])
@@ -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"), and not a number on a scale ("below zero")."""
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()):
@@ -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"))
@@ -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):
@@ -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"]}])
@@ -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"])
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