hypgraph 0.4.1__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.1 → hypgraph-0.4.2}/CHANGELOG.md +8 -0
  2. {hypgraph-0.4.1 → hypgraph-0.4.2}/PKG-INFO +1 -1
  3. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/__init__.py +1 -1
  4. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/language.py +13 -1
  5. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/report_check.py +11 -0
  6. hypgraph-0.4.2/tests/test_questions_not_assertions.py +43 -0
  7. {hypgraph-0.4.1 → hypgraph-0.4.2}/.gitignore +0 -0
  8. {hypgraph-0.4.1 → hypgraph-0.4.2}/AGENTS.md +0 -0
  9. {hypgraph-0.4.1 → hypgraph-0.4.2}/CITATION.cff +0 -0
  10. {hypgraph-0.4.1 → hypgraph-0.4.2}/LICENSE +0 -0
  11. {hypgraph-0.4.1 → hypgraph-0.4.2}/NOTICE +0 -0
  12. {hypgraph-0.4.1 → hypgraph-0.4.2}/README.md +0 -0
  13. {hypgraph-0.4.1 → hypgraph-0.4.2}/docs/PLAN.md +0 -0
  14. {hypgraph-0.4.1 → hypgraph-0.4.2}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
  15. {hypgraph-0.4.1 → hypgraph-0.4.2}/docs/decisions/ADR-002-governance-profiles.md +0 -0
  16. {hypgraph-0.4.1 → hypgraph-0.4.2}/docs/review-2026-10-24.md +0 -0
  17. {hypgraph-0.4.1 → hypgraph-0.4.2}/docs/spec.md +0 -0
  18. {hypgraph-0.4.1 → hypgraph-0.4.2}/pyproject.toml +0 -0
  19. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/__main__.py +0 -0
  20. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/bundle.py +0 -0
  21. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/checks.py +0 -0
  22. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/cli.py +0 -0
  23. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/coord.py +0 -0
  24. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/demos/__init__.py +0 -0
  25. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/demos/broad_street.py +0 -0
  26. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/derive.py +0 -0
  27. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/graph.py +0 -0
  28. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/mcp.py +0 -0
  29. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/plan.py +0 -0
  30. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/policy.py +0 -0
  31. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/profiles.py +0 -0
  32. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/refusals.py +0 -0
  33. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/schema.py +0 -0
  34. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/server.py +0 -0
  35. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/skill/SKILL.md +0 -0
  36. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/stats.py +0 -0
  37. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/store.py +0 -0
  38. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/ui/__init__.py +0 -0
  39. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/ui/atlas.css +0 -0
  40. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/ui/atlas.js +0 -0
  41. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/ui/hypgraph.svg +0 -0
  42. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/ui/index.html +0 -0
  43. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
  44. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/ui/vendor/cose-base.js +0 -0
  45. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
  46. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
  47. {hypgraph-0.4.1 → hypgraph-0.4.2}/src/hypgraph/ui/vendor/layout-base.js +0 -0
  48. {hypgraph-0.4.1 → hypgraph-0.4.2}/studies/harmony/tests/test_suite.py +0 -0
  49. {hypgraph-0.4.1 → hypgraph-0.4.2}/tests/test_cancel_releases_lease.py +0 -0
  50. {hypgraph-0.4.1 → hypgraph-0.4.2}/tests/test_coord.py +0 -0
  51. {hypgraph-0.4.1 → hypgraph-0.4.2}/tests/test_d10.py +0 -0
  52. {hypgraph-0.4.1 → hypgraph-0.4.2}/tests/test_engine.py +0 -0
  53. {hypgraph-0.4.1 → hypgraph-0.4.2}/tests/test_engine_fixes.py +0 -0
  54. {hypgraph-0.4.1 → hypgraph-0.4.2}/tests/test_incremental.py +0 -0
  55. {hypgraph-0.4.1 → hypgraph-0.4.2}/tests/test_log_and_check.py +0 -0
  56. {hypgraph-0.4.1 → hypgraph-0.4.2}/tests/test_order_detail.py +0 -0
  57. {hypgraph-0.4.1 → hypgraph-0.4.2}/tests/test_profiles.py +0 -0
  58. {hypgraph-0.4.1 → hypgraph-0.4.2}/tests/test_profiles_041.py +0 -0
  59. {hypgraph-0.4.1 → hypgraph-0.4.2}/tests/test_refusals.py +0 -0
  60. {hypgraph-0.4.1 → hypgraph-0.4.2}/tests/test_server_auth.py +0 -0
  61. {hypgraph-0.4.1 → hypgraph-0.4.2}/tests/test_stats_values.py +0 -0
  62. {hypgraph-0.4.1 → hypgraph-0.4.2}/tests/test_surfaces.py +0 -0
@@ -1,5 +1,13 @@
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
+
3
11
  ## 0.4.1 — 2026-10-25
4
12
 
5
13
  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.1
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.1"
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
@@ -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()):
@@ -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,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
File without changes
File without changes