hypgraph 0.8.4__tar.gz → 0.8.5__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 (137) hide show
  1. {hypgraph-0.8.4 → hypgraph-0.8.5}/CHANGELOG.md +17 -0
  2. {hypgraph-0.8.4 → hypgraph-0.8.5}/PKG-INFO +1 -1
  3. {hypgraph-0.8.4 → hypgraph-0.8.5}/docs/PLAN.md +1 -1
  4. {hypgraph-0.8.4 → hypgraph-0.8.5}/docs/RELEASE-READINESS.md +6 -0
  5. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/__init__.py +1 -1
  6. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/checks.py +14 -3
  7. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/report_check.py +26 -3
  8. hypgraph-0.8.5/tests/test_report_significance_denied.py +62 -0
  9. hypgraph-0.8.5/tests/test_statement_slots.py +60 -0
  10. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_versions_not_figures.py +8 -0
  11. {hypgraph-0.8.4 → hypgraph-0.8.5}/.gitignore +0 -0
  12. {hypgraph-0.8.4 → hypgraph-0.8.5}/AGENTS.md +0 -0
  13. {hypgraph-0.8.4 → hypgraph-0.8.5}/CITATION.cff +0 -0
  14. {hypgraph-0.8.4 → hypgraph-0.8.5}/DISCLAIMER.md +0 -0
  15. {hypgraph-0.8.4 → hypgraph-0.8.5}/LICENSE +0 -0
  16. {hypgraph-0.8.4 → hypgraph-0.8.5}/NOTICE +0 -0
  17. {hypgraph-0.8.4 → hypgraph-0.8.5}/README.md +0 -0
  18. {hypgraph-0.8.4 → hypgraph-0.8.5}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
  19. {hypgraph-0.8.4 → hypgraph-0.8.5}/docs/decisions/ADR-002-governance-profiles.md +0 -0
  20. {hypgraph-0.8.4 → hypgraph-0.8.5}/docs/profile-workflows-plan.md +0 -0
  21. {hypgraph-0.8.4 → hypgraph-0.8.5}/docs/review-2026-10-24.md +0 -0
  22. {hypgraph-0.8.4 → hypgraph-0.8.5}/docs/site-plan.md +0 -0
  23. {hypgraph-0.8.4 → hypgraph-0.8.5}/docs/spec.md +0 -0
  24. {hypgraph-0.8.4 → hypgraph-0.8.5}/docs/ui-plan.md +0 -0
  25. {hypgraph-0.8.4 → hypgraph-0.8.5}/docs/verification.md +0 -0
  26. {hypgraph-0.8.4 → hypgraph-0.8.5}/docs/workflows/README.md +0 -0
  27. {hypgraph-0.8.4 → hypgraph-0.8.5}/docs/workflows/knowledge.md +0 -0
  28. {hypgraph-0.8.4 → hypgraph-0.8.5}/docs/workflows/optimization.md +0 -0
  29. {hypgraph-0.8.4 → hypgraph-0.8.5}/docs/workflows/scientific.md +0 -0
  30. {hypgraph-0.8.4 → hypgraph-0.8.5}/docs/workflows/software.md +0 -0
  31. {hypgraph-0.8.4 → hypgraph-0.8.5}/docs/workflows/trading.md +0 -0
  32. {hypgraph-0.8.4 → hypgraph-0.8.5}/examples/frameworks/README.md +0 -0
  33. {hypgraph-0.8.4 → hypgraph-0.8.5}/pyproject.toml +0 -0
  34. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/__main__.py +0 -0
  35. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/assertions.py +0 -0
  36. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/blind.py +0 -0
  37. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/bundle.py +0 -0
  38. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/cli.py +0 -0
  39. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/coord.py +0 -0
  40. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/demos/__init__.py +0 -0
  41. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/demos/broad_street.py +0 -0
  42. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/demos/dispute.py +0 -0
  43. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/demos/linked.py +0 -0
  44. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/derive.py +0 -0
  45. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/graph.py +0 -0
  46. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/language.py +0 -0
  47. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/mcp.py +0 -0
  48. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/plan.py +0 -0
  49. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/policy.py +0 -0
  50. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/profiles.py +0 -0
  51. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/references.py +0 -0
  52. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/refusals.py +0 -0
  53. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/schema.py +0 -0
  54. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/server.py +0 -0
  55. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/skill/SKILL.md +0 -0
  56. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/stats.py +0 -0
  57. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/store.py +0 -0
  58. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/ui/__init__.py +0 -0
  59. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/ui/atlas.css +0 -0
  60. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/ui/atlas.js +0 -0
  61. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/ui/hypgraph.css +0 -0
  62. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/ui/hypgraph.i18n.js +0 -0
  63. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/ui/hypgraph.js +0 -0
  64. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/ui/hypgraph.svg +0 -0
  65. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/ui/index.html +0 -0
  66. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
  67. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/ui/vendor/cose-base.js +0 -0
  68. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
  69. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
  70. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/ui/vendor/layout-base.js +0 -0
  71. {hypgraph-0.8.4 → hypgraph-0.8.5}/src/hypgraph/workflow.py +0 -0
  72. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_accept_work_kind.py +0 -0
  73. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_anchoring.py +0 -0
  74. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_assertions.py +0 -0
  75. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_blind.py +0 -0
  76. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_blind_oracles.py +0 -0
  77. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_borrowed_words.py +0 -0
  78. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_bound_support.py +0 -0
  79. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_bundle_telemetry.py +0 -0
  80. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_cancel_releases_lease.py +0 -0
  81. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_cited_and_failed_support.py +0 -0
  82. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_contradictions.py +0 -0
  83. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_coord.py +0 -0
  84. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_counts_that_bear.py +0 -0
  85. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_d10.py +0 -0
  86. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_demo_dispute.py +0 -0
  87. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_direction_and_units.py +0 -0
  88. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_engine.py +0 -0
  89. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_engine_fixes.py +0 -0
  90. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_figure_said_twice.py +0 -0
  91. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_framework_examples.py +0 -0
  92. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_i18n.py +0 -0
  93. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_incremental.py +0 -0
  94. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_link_translate.py +0 -0
  95. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_linked_records.py +0 -0
  96. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_log_and_check.py +0 -0
  97. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_measure_questions.py +0 -0
  98. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_next_order.py +0 -0
  99. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_opposed_answers.py +0 -0
  100. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_option_lead.py +0 -0
  101. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_order_detail.py +0 -0
  102. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_profiles.py +0 -0
  103. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_profiles_041.py +0 -0
  104. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_quantifiers_and_identity.py +0 -0
  105. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_questions_not_assertions.py +0 -0
  106. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_quoted_counterexample.py +0 -0
  107. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_references.py +0 -0
  108. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_refusal_friction.py +0 -0
  109. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_refusal_results.py +0 -0
  110. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_refusals.py +0 -0
  111. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_relation_words.py +0 -0
  112. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_report_answers.py +0 -0
  113. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_rerunnable.py +0 -0
  114. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_restates_scale.py +0 -0
  115. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_server_auth.py +0 -0
  116. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_server_hosts.py +0 -0
  117. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_shape_refusals.py +0 -0
  118. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_stats_not_backtests.py +0 -0
  119. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_stats_values.py +0 -0
  120. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_surfaces.py +0 -0
  121. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_telemetry_stream.py +0 -0
  122. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_trust_boundary.py +0 -0
  123. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_typed_audits.py +0 -0
  124. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_ui_app.py +0 -0
  125. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_ui_engine.py +0 -0
  126. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_ui_sources_focus.py +0 -0
  127. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_uncorrected_effect.py +0 -0
  128. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_unnamed_inputs.py +0 -0
  129. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_verify_complete.py +0 -0
  130. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_verify_lifecycle.py +0 -0
  131. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_verify_scope.py +0 -0
  132. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_verify_scoped_views.py +0 -0
  133. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_verify_trust.py +0 -0
  134. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_verify_views.py +0 -0
  135. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_vocabulary_friction.py +0 -0
  136. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_what_a_count_is_of.py +0 -0
  137. {hypgraph-0.8.4 → hypgraph-0.8.5}/tests/test_workflows.py +0 -0
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.8.5 — 2026-11-06: fewer false refusals
6
+
7
+ - A date a source writes as an ordinal is in the source: "the 14th of June 1887" accounts for "14 June 1887". It was refused as an unbound number, and a seat ended up with a claim saying the bridge opened "on a date".
8
+ - A statement with an unfilled slot is refused as STATEMENT_EMPTY: a template's `{date}`, or a variable's name where its value belongs (`accuracy = A_easy_acc`). Inline code is not read for this.
9
+ - **A report that denies significance isn't refused for it.** `REPORT_SIGNIFICANCE_UNBACKED` is a warning, not an
10
+ error, when the sentence denies the test: emphasis ("*not* significantly"), "no statistically significant difference",
11
+ "does not support a statistically significant advantage". Before, only the bare "not significant" counted, so an
12
+ honest negative was refused and small models gave up on the report. A sentence that also asserts significance
13
+ ("not only significantly", "B is not significantly cheaper, but A is significantly better") stays an error.
14
+ - **A named p-value carries the test.** A result whose values name one (`p`, `p_value`, `pvalue`, `p_exact`, …) backs a
15
+ significance word, and so does a claim whose statement gives a p-value or that rests on such a result. Before, only a
16
+ value with a `p` field did, so citing the claim that held the test was refused.
17
+ - **A word and a figure say something.** `STATEMENT_EMPTY` no longer fires on "A's accuracy is 0.814."; placeholders
18
+ still do, with a number beside them or not.
19
+
3
20
  ## 0.8.4 — 2026-11-04: sources in the sidebar; a focus on one node's part of the graph
4
21
 
5
22
  - **A Sources view in the Explorer's sidebar.** Everything an investigation cites, in one place: outside literature
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hypgraph
3
- Version: 0.8.4
3
+ Version: 0.8.5
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/
@@ -109,7 +109,7 @@ claim's derived standing.
109
109
  Multi-writer sync between graphs (bundles cover sharing), authentication (the host's job; the HTTP server binds to
110
110
  loopback and is read-only unless started with `--writable`), and a hosted service.
111
111
 
112
- ## Status at 0.8.4 (2026-11-04)
112
+ ## Status at 0.8.5 (2026-11-06)
113
113
 
114
114
  All seven phases are done: the library, bundles, the server and UI, the MCP server, the website, Pegasus running on
115
115
  the package, and releases to PyPI from tags with trusted publishing. What came after the plan, by release, is in
@@ -261,6 +261,12 @@ was taken from a changelog without being checked against the code.*
261
261
  > with two or more of them when the exporting and verifying processes hashed strings differently. The list now follows
262
262
  > the order the hypotheses were added. #109 is merged with green CI. Released as 0.8.3.
263
263
 
264
+ > **After 0.8.4 (2026-11-06).** Checks only, no change to the bundle format: false refusals found running HypSwarm on a
265
+ > small local model. A report that denies significance is warned, not refused; a named p-value carries the test; a word
266
+ > and a figure are a statement, and an unfilled slot isn't; an ordinal in a source accounts for the number. Tests:
267
+ > `tests/test_report_significance_denied.py`, `tests/test_statement_slots.py`, `tests/test_versions_not_figures.py`.
268
+ > #113 is merged with green CI. Released as 0.8.5.
269
+
264
270
  > **After 0.8.3 (2026-11-04).** Explorer only, no change to the engine, the checks or the bundle format: a Sources view
265
271
  > (what an investigation cites, with what the record did and did not check about each) and a focus on one node's part
266
272
  > of the graph, carried in the link. The focus is computed in the browser from the record's own edges;
@@ -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.8.4"
19
+ __version__ = "0.8.5"
20
20
 
21
21
  from .bundle import anchored, verify_bundle # noqa: E402
22
22
  from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
@@ -1598,6 +1598,9 @@ def _exclusion(s: Snap, stmt: str, kind: str, support: list[str], bound: list) -
1598
1598
  "effect", words=words)]
1599
1599
 
1600
1600
 
1601
+ _ORDINAL = re.compile(r"\b(\d+)(?:st|nd|rd|th)\b", re.I)
1602
+ # a value not filled in: a template's {date}, or a variable's name where its value belongs ("accuracy = A_easy_acc")
1603
+ _SLOT = re.compile(r"\{\s*[A-Za-z_][\w.]*\s*\}|=\s*[A-Za-z]+_[A-Za-z0-9_]*\b(?!\s*\()")
1601
1604
  _PLACEHOLDER = re.compile(r"^\W*(?:(?:placeholder|tbd|todo|tba|n/?a|none|wip|xxx|test|claim|statement|pending|"
1602
1605
  r"to be (?:determined|confirmed|added)|lorem ipsum)\W*)+$", re.I)
1603
1606
 
@@ -1776,13 +1779,20 @@ def claim_issues(s: Snap, body: dict, support: list[str], inv: str) -> list[Prob
1776
1779
  if invb.get("strict_claims") is False:
1777
1780
  return []
1778
1781
  problems: list[Problem] = []
1779
- if _PLACEHOLDER.match(stmt) or len(re.findall(r"[^\W\d_]{3,}", stmt)) < 2:
1780
- # a claim written to clear a gate (studies/challenges E2: "Placeholder.", contested and left in the record)
1782
+ words = len(re.findall(r"[^\W\d_]{3,}", stmt))
1783
+ if _PLACEHOLDER.match(_NUM.sub("", stmt)) or words < (1 if _NUM.search(stmt) else 2):
1784
+ # a claim written to clear a gate (studies/challenges E2: "Placeholder.", contested and left in the record);
1785
+ # a word and a figure do say something: "A's accuracy is 0.814."
1781
1786
  problems.append(Problem("STATEMENT_EMPTY", "the statement says nothing: write the finding itself — what was "
1782
1787
  "measured, on what, and what it came to"))
1783
1788
  if kind in ("finding", "exclusion") and len(scope.strip()) < 10:
1784
1789
  problems.append(Problem("SCOPE_MISSING", "scope: say under which conditions this holds (the data, method, "
1785
1790
  "sample) — at least a sentence"))
1791
+ slot = _SLOT.search(re.sub(r"`[^`]*`", " ", stmt))
1792
+ if slot:
1793
+ # refused the date, a seat wrote "opened on {date}", and a later version "on a date" was checked as the answer
1794
+ problems.append(Problem("STATEMENT_EMPTY", f"the statement has an unfilled slot ({slot.group(0).strip()}): "
1795
+ "write the value itself", words=[slot.group(0).strip()]))
1786
1796
  if kind == "convention" and L.HEDGE.search(stmt):
1787
1797
  problems.append(Problem("CONVENTION_HEDGED", "a convention states agreed notation; a hedged interpretation "
1788
1798
  "(could / suggests / consistent with) is a finding or a lead"))
@@ -1799,7 +1809,8 @@ def claim_issues(s: Snap, body: dict, support: list[str], inv: str) -> list[Prob
1799
1809
  [float(c) for c in (invb.get("constants") or []) if isinstance(c, (int, float))]
1800
1810
  for src in support: # a cited source's own text accounts for the numbers in it
1801
1811
  sb = s.nodes[src]["body"] if s.nodes[src]["type"] == "source" else {}
1802
- allowed += [v for v, *_ in numbers(" ".join(str(sb.get(k) or "") for k in ("passage", "citation", "title")))]
1812
+ said = " ".join(str(sb.get(k) or "") for k in ("passage", "citation", "title"))
1813
+ allowed += [v for v, *_ in numbers(_ORDINAL.sub(r"\1", said))] # "the 14th of June" accounts for "14 June"
1803
1814
  if _CONVENTIONS.search(clean): # "95% interval", "α 0.05", "80% power" are conventions
1804
1815
  allowed += [95.0, 99.0, 90.0, 80.0, 0.05, 0.01]
1805
1816
  loose = [p for p in numbers(clean) if not _matches(p, allowed) and not _matches_stat(p, stat)]
@@ -92,6 +92,28 @@ def _asserted(pattern: re.Pattern, text: str) -> list[str]:
92
92
  return [m.group(0).lower() for m in pattern.finditer(text) if not _NEGATOR.search(text[:m.start()])]
93
93
 
94
94
 
95
+ _P_KEY = re.compile(r"p(?:[_-]?val(?:ue)?|_[a-z0-9_]+)?", re.I)
96
+
97
+
98
+ def _carries_test(s: D.Snap, nid: str) -> bool:
99
+ """Does this node hold a test: a two-sample count the record worked out, a value with a p, a value named as one
100
+ (p_value, p_exact, pvalue), or, for a claim, its own statement's p-value or any such result it rests on?"""
101
+ def holds(n: str) -> bool:
102
+ vals = s.nodes[n]["body"].get("values") or {}
103
+ return bool(D.state(s, n).get("test")) or any(
104
+ _P_KEY.fullmatch(str(k)) or (isinstance(v, dict) and "p" in v) for k, v in vals.items())
105
+ if holds(nid):
106
+ return True
107
+ n = s.nodes[nid]
108
+ if n["type"] != "claim":
109
+ return False
110
+ if _P_VALUE.search(n["body"].get("statement") or ""):
111
+ return True
112
+ bound = [m.get("result") for m in n["body"].get("measurements") or [] if isinstance(m, dict)]
113
+ rests = [e["dst"] for e in s.out.get(nid, []) if e["rel"] == "derived_from"]
114
+ return any(holds(r) for r in [*bound, *rests] if r in s.nodes)
115
+
116
+
95
117
  def _blank(m: re.Match) -> str:
96
118
  return re.sub(r"[^\n]", " ", m.group(0))
97
119
 
@@ -286,9 +308,10 @@ def check(g: "Graph", text: str, inv: str = "") -> dict:
286
308
  f"'{causal[0]}' is causal and no claim is cited — cite the claim backed by an experiment, or say "
287
309
  "'is associated with'.")
288
310
  sig = L.found(L.SIGNIFICANCE, own)
289
- if sig and not _P_VALUE.search(sent) and not any(D.state(s, c).get("test") or any(isinstance(v, dict) and "p" in v for v in
290
- (s.nodes[c]["body"].get("values") or {}).values()) for c in cited):
291
- negated = re.search(r"\bnot\s+(?:statistically\s+)?(?:significant|unusual)|\bnon-?significant", sent, re.I)
311
+ if sig and not _P_VALUE.search(sent) and not any(_carries_test(s, c) for c in cited):
312
+ # the sentence denies the test rather than claims it: emphasis ("*not* significantly") is no assertion
313
+ plain = re.sub(r"\bnot\s+(?:only|just|merely)\b", "", re.sub(r"[*_]", "", own), flags=re.I)
314
+ negated = bool(L.denied(L.SIGNIFICANCE, plain)) and not L.asserted(L.SIGNIFICANCE, plain)
292
315
  add(ln, "warning" if negated else "error", "REPORT_SIGNIFICANCE_UNBACKED", sent,
293
316
  f"'{sig[0]}' — cite the claim or result that carries the test.")
294
317
  _check_answers(s, text or "", invs, by_code, add, counted)
@@ -0,0 +1,62 @@
1
+ """0.8.5: a report that says a difference was NOT significant isn't claiming a test passed, and a claim resting on a
2
+ named p-value carries its test. From gpt-oss runs on a small box: leads wrote "*not* significantly" and were refused
3
+ until they gave up, so two of six runs ended with no interpretation."""
4
+ import pytest
5
+
6
+ from hypgraph import Graph, Policy
7
+
8
+
9
+ def _record(values):
10
+ g = Graph(policy=Policy(reviewers={"critic"}))
11
+ inv = g.create(author="h", type="investigation", fields={"title": "Does A beat B.", "objective": "Compare A and B on the same items."})["id"]
12
+ q = g.create(author="h", type="question", inv=inv, code="Q2",
13
+ fields={"text": "Is A significantly more accurate than B (two-sided, 0.05)?"})["id"]
14
+ r = g.create(author="stats", type="run", inv=inv, fields={"kind": "agent", "name": "mcnemar", "state": "succeeded"})
15
+ rs = g.create(author="stats", type="result", inv=inv, fields={"summary": "McNemar test on the same 1,000 items.", "values": values},
16
+ links=[{"rel": "produced", "from": r["id"]}])
17
+ c = g.create(author="stats", type="claim", inv=inv,
18
+ fields={"statement": "The McNemar test on the 1,000 shared items gave the value in the result.",
19
+ "scope": "The 1,000 shared items."},
20
+ links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs["id"]}])
21
+ return g, inv, c["code"], rs["code"]
22
+
23
+
24
+ def _sig(g, text, inv):
25
+ return [f["level"] for f in g.check_text(text, inv)["findings"] if f["code"] == "REPORT_SIGNIFICANCE_UNBACKED"]
26
+
27
+
28
+ @pytest.mark.parametrize("sentence", [
29
+ "Model A is *not* significantly more accurate than model B.",
30
+ "Model A is **not** significantly more accurate than model B.",
31
+ "There is no statistically significant difference between A and B.",
32
+ "The evidence does not support a statistically significant advantage for A.",
33
+ "The test did not reach statistical significance, so A is not significantly better.",
34
+ ])
35
+ def test_a_denial_is_a_warning_not_a_refusal(sentence):
36
+ g, inv, _, _ = _record({"chi2": {"value": 3.0}})
37
+ assert _sig(g, sentence, inv) in (["warning"], [])
38
+ assert g.check_text(sentence, inv)["errors"] == 0
39
+
40
+
41
+ @pytest.mark.parametrize("sentence", [
42
+ "A is significantly more accurate than B.",
43
+ "A is not only significantly more accurate than B, it is cheaper.",
44
+ "A is not worse, and it is significantly more accurate than B.",
45
+ "B is not significantly cheaper, but A is significantly more accurate.",
46
+ ])
47
+ def test_an_assertion_is_still_refused(sentence):
48
+ g, inv, _, _ = _record({"chi2": {"value": 3.0}})
49
+ assert _sig(g, sentence, inv) == ["error"]
50
+
51
+
52
+ @pytest.mark.parametrize("key", ["p", "p_value", "pvalue", "p-value", "p_exact", "p_two"])
53
+ def test_a_named_p_value_carries_the_test(key):
54
+ g, inv, cl, rs = _record({key: {"value": 0.03}})
55
+ assert _sig(g, f"A is significantly more accurate than B ({rs}).", inv) == [] # the result itself
56
+ assert _sig(g, f"A is significantly more accurate than B ({cl}).", inv) == [] # the claim that rests on it
57
+
58
+
59
+ def test_a_claim_without_a_test_does_not_carry_one():
60
+ g, inv, cl, rs = _record({"pct_a": {"value": 90.0}, "pairs": {"value": 322}, "chi2": {"value": 3.0}})
61
+ assert _sig(g, f"A is significantly more accurate than B ({cl}).", inv) == ["error"]
62
+ assert _sig(g, f"A is significantly more accurate than B ({rs}).", inv) == ["error"]
@@ -0,0 +1,60 @@
1
+ """A number a source writes as an ordinal is in the source, and a statement with a slot in it says nothing. Found on a
2
+ BC-250 run of the knowledge challenge: the passage said "the 14th of June 1887", "14 June 1887" was refused as unbound
3
+ 12 times, the seat wrote "opened on {date}", then "opened on a date", and that was checked as the answer."""
4
+ from __future__ import annotations
5
+
6
+ import pytest
7
+
8
+ from hypgraph import Graph, GraphError, Policy
9
+
10
+
11
+ def _record():
12
+ g = Graph(policy=Policy(reviewers={"critic"}))
13
+ inv = g.create(author="a", type="investigation", fields={
14
+ "title": "The footbridge", "objective": "When did the footbridge open?", "profile": "knowledge@2"})["id"]
15
+ src = g.create(author="a", type="source", inv=inv, fields={
16
+ "url": "file:archive/minutes.md", "kind": "primary", "retrieved": "2026-11-05",
17
+ "citation": "Council minutes, 2 May",
18
+ "passage": "Resolved that the bridge be opened to the public on Tuesday the 14th of June 1887."})["id"]
19
+ return g, inv, src
20
+
21
+
22
+ def _claim(g, inv, src, statement):
23
+ return g.create(author="a", type="claim", inv=inv, fields={
24
+ "statement": statement, "scope": "The council minutes of the town, read in full."},
25
+ links=[{"rel": "cites", "to": src}, {"rel": "supports", "from": src, "why": "the minutes name the day it opened"}])
26
+
27
+
28
+ @pytest.mark.parametrize("statement", ["The footbridge opened to the public on 14 June 1887.",
29
+ "The footbridge opened to the public on the 14th of June 1887."])
30
+ def test_a_date_written_as_an_ordinal_is_in_the_source(statement):
31
+ g, inv, src = _record()
32
+ assert _claim(g, inv, src, statement)["id"]
33
+
34
+
35
+ def test_a_number_the_source_doesnt_have_stays_unbound():
36
+ g, inv, src = _record()
37
+ with pytest.raises(GraphError, match="UNBOUND_NUMBER"):
38
+ _claim(g, inv, src, "The footbridge opened to the public on 15 June 1887.")
39
+
40
+
41
+ @pytest.mark.parametrize("statement", [
42
+ "The footbridge opened to the public on {date}.",
43
+ "The footbridge opened to the public on { date }.",
44
+ "Model A easy accuracy = A_easy_acc on the items.",
45
+ "The strategy with fast window {fast_window} returned the most.",
46
+ ])
47
+ def test_a_statement_with_a_slot_is_refused(statement):
48
+ g, inv, src = _record()
49
+ with pytest.raises(GraphError, match="STATEMENT_EMPTY"):
50
+ _claim(g, inv, src, statement)
51
+
52
+
53
+ @pytest.mark.parametrize("statement", [
54
+ "The minutes set the option `retry = max_tries` for the footbridge.",
55
+ "The function returned `{date}` for the footbridge.",
56
+ "The footbridge opened in the summer, as the minutes say.",
57
+ ])
58
+ def test_code_and_plain_sentences_are_not_slots(statement):
59
+ g, inv, src = _record()
60
+ assert _claim(g, inv, src, statement)["id"]
@@ -76,3 +76,11 @@ def test_a_statement_that_says_nothing_is_refused(tmp_path):
76
76
  for i, stmt in enumerate(("Placeholder.", "Placeholder claim.", "TBD — pending.", "To be determined.", "1234567890 ...")):
77
77
  assert "STATEMENT_EMPTY" in _claim_error(tmp_path / f"{i}.hypgraph", stmt), stmt
78
78
  assert _claim_error(tmp_path / "real.hypgraph", "parse_duration truncates rather than rounds.") == ""
79
+
80
+
81
+ def test_a_word_and_a_figure_say_something(tmp_path):
82
+ """From gpt-oss runs: "A's accuracy is 0.814." has one word of three letters, and was refused as saying nothing."""
83
+ for i, stmt in enumerate(("A's accuracy is 0.814.", "Accuracy: 81.4 %.")):
84
+ assert "STATEMENT_EMPTY" not in _claim_error(tmp_path / f"{i}.hypgraph", stmt), stmt
85
+ for i, stmt in enumerate(("TBD 12.", "Placeholder 3.", "Placeholder 3 — pending.", "See RS1.", "12.")):
86
+ assert _claim_error(tmp_path / f"e{i}.hypgraph", stmt), stmt # some are too short to reach this check
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