hypgraph 0.8.5__tar.gz → 0.8.7__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 (139) hide show
  1. {hypgraph-0.8.5 → hypgraph-0.8.7}/CHANGELOG.md +32 -0
  2. {hypgraph-0.8.5 → hypgraph-0.8.7}/PKG-INFO +1 -1
  3. {hypgraph-0.8.5 → hypgraph-0.8.7}/docs/PLAN.md +1 -1
  4. {hypgraph-0.8.5 → hypgraph-0.8.7}/docs/RELEASE-READINESS.md +16 -0
  5. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/__init__.py +1 -1
  6. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/checks.py +15 -3
  7. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/graph.py +49 -19
  8. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/workflow.py +5 -0
  9. hypgraph-0.8.7/tests/test_consistent_load.py +76 -0
  10. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_measure_questions.py +16 -2
  11. hypgraph-0.8.7/tests/test_statement_is_a_question.py +51 -0
  12. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_statement_slots.py +23 -1
  13. {hypgraph-0.8.5 → hypgraph-0.8.7}/.gitignore +0 -0
  14. {hypgraph-0.8.5 → hypgraph-0.8.7}/AGENTS.md +0 -0
  15. {hypgraph-0.8.5 → hypgraph-0.8.7}/CITATION.cff +0 -0
  16. {hypgraph-0.8.5 → hypgraph-0.8.7}/DISCLAIMER.md +0 -0
  17. {hypgraph-0.8.5 → hypgraph-0.8.7}/LICENSE +0 -0
  18. {hypgraph-0.8.5 → hypgraph-0.8.7}/NOTICE +0 -0
  19. {hypgraph-0.8.5 → hypgraph-0.8.7}/README.md +0 -0
  20. {hypgraph-0.8.5 → hypgraph-0.8.7}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
  21. {hypgraph-0.8.5 → hypgraph-0.8.7}/docs/decisions/ADR-002-governance-profiles.md +0 -0
  22. {hypgraph-0.8.5 → hypgraph-0.8.7}/docs/profile-workflows-plan.md +0 -0
  23. {hypgraph-0.8.5 → hypgraph-0.8.7}/docs/review-2026-10-24.md +0 -0
  24. {hypgraph-0.8.5 → hypgraph-0.8.7}/docs/site-plan.md +0 -0
  25. {hypgraph-0.8.5 → hypgraph-0.8.7}/docs/spec.md +0 -0
  26. {hypgraph-0.8.5 → hypgraph-0.8.7}/docs/ui-plan.md +0 -0
  27. {hypgraph-0.8.5 → hypgraph-0.8.7}/docs/verification.md +0 -0
  28. {hypgraph-0.8.5 → hypgraph-0.8.7}/docs/workflows/README.md +0 -0
  29. {hypgraph-0.8.5 → hypgraph-0.8.7}/docs/workflows/knowledge.md +0 -0
  30. {hypgraph-0.8.5 → hypgraph-0.8.7}/docs/workflows/optimization.md +0 -0
  31. {hypgraph-0.8.5 → hypgraph-0.8.7}/docs/workflows/scientific.md +0 -0
  32. {hypgraph-0.8.5 → hypgraph-0.8.7}/docs/workflows/software.md +0 -0
  33. {hypgraph-0.8.5 → hypgraph-0.8.7}/docs/workflows/trading.md +0 -0
  34. {hypgraph-0.8.5 → hypgraph-0.8.7}/examples/frameworks/README.md +0 -0
  35. {hypgraph-0.8.5 → hypgraph-0.8.7}/pyproject.toml +0 -0
  36. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/__main__.py +0 -0
  37. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/assertions.py +0 -0
  38. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/blind.py +0 -0
  39. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/bundle.py +0 -0
  40. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/cli.py +0 -0
  41. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/coord.py +0 -0
  42. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/demos/__init__.py +0 -0
  43. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/demos/broad_street.py +0 -0
  44. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/demos/dispute.py +0 -0
  45. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/demos/linked.py +0 -0
  46. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/derive.py +0 -0
  47. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/language.py +0 -0
  48. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/mcp.py +0 -0
  49. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/plan.py +0 -0
  50. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/policy.py +0 -0
  51. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/profiles.py +0 -0
  52. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/references.py +0 -0
  53. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/refusals.py +0 -0
  54. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/report_check.py +0 -0
  55. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/schema.py +0 -0
  56. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/server.py +0 -0
  57. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/skill/SKILL.md +0 -0
  58. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/stats.py +0 -0
  59. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/store.py +0 -0
  60. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/ui/__init__.py +0 -0
  61. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/ui/atlas.css +0 -0
  62. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/ui/atlas.js +0 -0
  63. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/ui/hypgraph.css +0 -0
  64. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/ui/hypgraph.i18n.js +0 -0
  65. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/ui/hypgraph.js +0 -0
  66. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/ui/hypgraph.svg +0 -0
  67. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/ui/index.html +0 -0
  68. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
  69. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/ui/vendor/cose-base.js +0 -0
  70. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
  71. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
  72. {hypgraph-0.8.5 → hypgraph-0.8.7}/src/hypgraph/ui/vendor/layout-base.js +0 -0
  73. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_accept_work_kind.py +0 -0
  74. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_anchoring.py +0 -0
  75. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_assertions.py +0 -0
  76. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_blind.py +0 -0
  77. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_blind_oracles.py +0 -0
  78. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_borrowed_words.py +0 -0
  79. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_bound_support.py +0 -0
  80. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_bundle_telemetry.py +0 -0
  81. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_cancel_releases_lease.py +0 -0
  82. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_cited_and_failed_support.py +0 -0
  83. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_contradictions.py +0 -0
  84. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_coord.py +0 -0
  85. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_counts_that_bear.py +0 -0
  86. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_d10.py +0 -0
  87. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_demo_dispute.py +0 -0
  88. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_direction_and_units.py +0 -0
  89. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_engine.py +0 -0
  90. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_engine_fixes.py +0 -0
  91. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_figure_said_twice.py +0 -0
  92. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_framework_examples.py +0 -0
  93. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_i18n.py +0 -0
  94. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_incremental.py +0 -0
  95. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_link_translate.py +0 -0
  96. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_linked_records.py +0 -0
  97. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_log_and_check.py +0 -0
  98. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_next_order.py +0 -0
  99. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_opposed_answers.py +0 -0
  100. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_option_lead.py +0 -0
  101. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_order_detail.py +0 -0
  102. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_profiles.py +0 -0
  103. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_profiles_041.py +0 -0
  104. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_quantifiers_and_identity.py +0 -0
  105. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_questions_not_assertions.py +0 -0
  106. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_quoted_counterexample.py +0 -0
  107. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_references.py +0 -0
  108. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_refusal_friction.py +0 -0
  109. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_refusal_results.py +0 -0
  110. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_refusals.py +0 -0
  111. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_relation_words.py +0 -0
  112. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_report_answers.py +0 -0
  113. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_report_significance_denied.py +0 -0
  114. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_rerunnable.py +0 -0
  115. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_restates_scale.py +0 -0
  116. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_server_auth.py +0 -0
  117. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_server_hosts.py +0 -0
  118. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_shape_refusals.py +0 -0
  119. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_stats_not_backtests.py +0 -0
  120. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_stats_values.py +0 -0
  121. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_surfaces.py +0 -0
  122. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_telemetry_stream.py +0 -0
  123. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_trust_boundary.py +0 -0
  124. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_typed_audits.py +0 -0
  125. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_ui_app.py +0 -0
  126. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_ui_engine.py +0 -0
  127. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_ui_sources_focus.py +0 -0
  128. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_uncorrected_effect.py +0 -0
  129. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_unnamed_inputs.py +0 -0
  130. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_verify_complete.py +0 -0
  131. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_verify_lifecycle.py +0 -0
  132. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_verify_scope.py +0 -0
  133. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_verify_scoped_views.py +0 -0
  134. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_verify_trust.py +0 -0
  135. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_verify_views.py +0 -0
  136. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_versions_not_figures.py +0 -0
  137. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_vocabulary_friction.py +0 -0
  138. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_what_a_count_is_of.py +0 -0
  139. {hypgraph-0.8.5 → hypgraph-0.8.7}/tests/test_workflows.py +0 -0
@@ -2,6 +2,38 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.8.7 — 2026-11-08: a snapshot is loaded at one moment
6
+
7
+ No change to the bundle format or the checks: one read fix and one scope fix, both found running HypSwarm.
8
+
9
+ - **A snapshot is loaded at one moment.** The tables behind a snapshot were each read in their own implicit
10
+ transaction, so a second Graph on the same file could commit a node and its edge between the `nodes` read and
11
+ the `edges` read; the edge then named a node the snapshot didn't hold, was skipped, and its mark was advanced
12
+ past it — that Graph never saw the edge again until a rebuild. hypswarm's scheduler and the MCP server an
13
+ external seat works through share a file this way (one process, two threads); on Ubuntu CI the scheduler found
14
+ "no live claim" on a question every other Graph showed answered, reworked the order, and asked the seat to count
15
+ again, which the record refused as a restatement (2026-11-05 to 11-08, never on Windows). A rebuild, a delta
16
+ (with its retraction check) and the row load now run inside one read transaction; a load inside a write's own
17
+ transaction joins it. Reproduced deterministically before the fix (`tests/test_consistent_load.py`).
18
+ - **The run behind a result on a question is a run on the question.** A question's scope collected results through
19
+ its claims (`derived_from`, `supports`) but runs only through an experiment's `executes` or a `part_of` link. A
20
+ harness that registers run → result → claim and links only the claim to the question left the run gate reading
21
+ "no run executes an experiment on Q1" with the result and the claim both met; the harness issued a `run` order
22
+ every round beside the review, and on one BC-250 run the seats filed 11 claims on 3 questions without settling
23
+ one (S3, 2026-11-07). The scope now adds the runs that `produced` its results.
24
+
25
+ ## 0.8.6 — 2026-11-07: a placeholder is not a statement
26
+
27
+ - **A figure's name is not the figure.** The slot check (`STATEMENT_EMPTY`) now refuses `$mean_tutored`,
28
+ `$cost_per_change$`, `<claim_id>`, and a bare name shaped like a figure standing where the figure belongs ("a
29
+ p-value steps_k_p", "before costs of cum_rule_before"). Over every BC-250 challenge record (1,535 claims) 13 were
30
+ written this way and reviewers checked three of them, one with a note listing the numbers the claim left out. A
31
+ name beside its value passes: "17.13 % (oos_return)", "oos_return = 0.17", "a mean_diff of 1.7"; a column named
32
+ `steps_k` is a name, not a slot.
33
+ - **A claim that is a question says nothing.** A statement ending in a question mark is refused as `STATEMENT_EMPTY`.
34
+ On a BC-250 run a seat wrote the question's own text as a claim superseding its right answer, and the reviewer
35
+ checked it, so the question settled on a question. A question followed by its answer passes as before.
36
+
5
37
  ## 0.8.5 — 2026-11-06: fewer false refusals
6
38
 
7
39
  - 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".
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hypgraph
3
- Version: 0.8.5
3
+ Version: 0.8.7
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.5 (2026-11-06)
112
+ ## Status at 0.8.7 (2026-11-08)
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,22 @@ 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.5 (2026-11-07).** Checks only, no change to the bundle format: two ways a claim said nothing and got
265
+ > checked on the BC-250 sweeps. A statement ending in a question mark is refused (a seat wrote the question's own text
266
+ > as a claim superseding its right answer, and the question settled on a question); a figure's name standing where the
267
+ > figure belongs is refused (`$mean_tutored`, `steps_k_p`, `cum_rule_before` — 13 such claims over 1,535, three of
268
+ > them checked), while a name beside its value passes. Tests: `tests/test_statement_slots.py`,
269
+ > `tests/test_statement_is_a_question.py`. #115 and #116 are merged with green CI. Released as 0.8.6.
270
+
271
+ > **After 0.8.6 (2026-11-08).** No change to the bundle format or the checks. A snapshot's tables were each read in
272
+ > their own implicit transaction, so a second Graph on the same file could commit a node and its edge between the
273
+ > `nodes` read and the `edges` read; the edge was skipped and its mark advanced past it, lost to that Graph until a
274
+ > rebuild (HypSwarm's scheduler beside its MCP server, on Ubuntu CI). Rebuild, delta and row load now run inside one
275
+ > read transaction; `tests/test_consistent_load.py` reproduces the loss on the old code. And a question's scope
276
+ > counts the runs that `produced` its results, so a harness that links only the claim to the question no longer sees
277
+ > "no run executes an experiment" with the result and claim both met. #118 and #119 are merged with green CI.
278
+ > Released as 0.8.7.
279
+
264
280
  > **After 0.8.4 (2026-11-06).** Checks only, no change to the bundle format: false refusals found running HypSwarm on a
265
281
  > small local model. A report that denies significance is warned, not refused; a named p-value carries the test; a word
266
282
  > and a figure are a statement, and an unfilled slot isn't; an ordinal in a source accounts for the number. Tests:
@@ -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.5"
19
+ __version__ = "0.8.7"
20
20
 
21
21
  from .bundle import anchored, verify_bundle # noqa: E402
22
22
  from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
@@ -4,7 +4,7 @@ and each refusal does the arithmetic the writer skipped, then offers the honest
4
4
 
5
5
  code refused when
6
6
  ─────────────────────────────────────────────────────────────────────────────────────────────────────────────
7
- STATEMENT_EMPTY the statement says nothing ("Placeholder.", "TBD", one word)
7
+ STATEMENT_EMPTY the statement says nothing ("Placeholder.", "TBD", one word, a question)
8
8
  SCOPE_MISSING a finding or exclusion doesn't say under which conditions it holds
9
9
  EVIDENCE_MISSING nothing supports it
10
10
  MEASUREMENT_MALFORMED a measurement doesn't name its result and field
@@ -1599,8 +1599,14 @@ def _exclusion(s: Snap, stmt: str, kind: str, support: list[str], bound: list) -
1599
1599
 
1600
1600
 
1601
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*\()")
1602
+ # a value not filled in: a template's {date} / $mean_tutored / $cost_per_change$ / <claim_id>, a variable's name where
1603
+ # its value belongs ("accuracy = A_easy_acc"), or a figure's name standing in the sentence as if it were the figure
1604
+ # ("a p-value steps_k_p", "before costs of cum_rule_before"; BC-250 sweeps 2026-11-05..07: 13 of 1,535 claims, three
1605
+ # of them checked by reviewers whose notes listed the numbers the claim left out). A name beside its value is not a
1606
+ # slot: "17.13 % (oos_return)", "oos_return = 0.17", "a mean_diff of 1.7"; a column named steps_k is a name
1607
+ _FIGURE_SUFFIX = r"p|pvalue|p_value|diff|before|after|return|mean|value|low|high|ci|count|ratio|gain|sd|se"
1608
+ _SLOT = re.compile(r"\{\s*[A-Za-z_][\w.]*\s*\}|\$\{?[A-Za-z_]\w*\}?\$?|<[A-Za-z_]\w*>|=\s*[A-Za-z]+_[A-Za-z0-9_]*\b(?!\s*\()"
1609
+ rf"|(?<![\w`(])[A-Za-z]+(?:_[A-Za-z]+)*_(?:{_FIGURE_SUFFIX})\b(?!\s*(?:[=:≈(\d]|is|was|of|at)\s*[-+−]?\d)(?!\s*\))")
1604
1610
  _PLACEHOLDER = re.compile(r"^\W*(?:(?:placeholder|tbd|todo|tba|n/?a|none|wip|xxx|test|claim|statement|pending|"
1605
1611
  r"to be (?:determined|confirmed|added)|lorem ipsum)\W*)+$", re.I)
1606
1612
 
@@ -1793,6 +1799,12 @@ def claim_issues(s: Snap, body: dict, support: list[str], inv: str) -> list[Prob
1793
1799
  # refused the date, a seat wrote "opened on {date}", and a later version "on a date" was checked as the answer
1794
1800
  problems.append(Problem("STATEMENT_EMPTY", f"the statement has an unfilled slot ({slot.group(0).strip()}): "
1795
1801
  "write the value itself", words=[slot.group(0).strip()]))
1802
+ if stmt.rstrip().rstrip("'\")").endswith("?"):
1803
+ # a seat wrote the question's own text as the claim, as superseding its right answer, and the reviewer checked
1804
+ # it ("the test failures confirm the claim"): the right answer was buried under a question mark (BC-250,
1805
+ # 2026-11-07, E1). A question followed by its answer still ends in a sentence, and passes
1806
+ problems.append(Problem("STATEMENT_EMPTY", "the statement is a question: write the answer to it — what was "
1807
+ "found, on what, and what it came to"))
1796
1808
  if kind == "convention" and L.HEDGE.search(stmt):
1797
1809
  problems.append(Problem("CONVENTION_HEDGED", "a convention states agreed notation; a hedged interpretation "
1798
1810
  "(could / suggests / consistent with) is a finding or a lead"))
@@ -394,15 +394,32 @@ class Graph:
394
394
 
395
395
  def _rebuild(self, con) -> D.Snap:
396
396
  s = D.Snap(0, self.policy)
397
- retracted = {r[0] for r in con.execute("SELECT edge FROM retractions")}
398
- self._load_rows(con, s, retracted)
399
- s.seq = max(self._head_seq(con), s.seq)
397
+ own_txn = not con.in_transaction # retractions, rows and the head seq read at one moment
398
+ if own_txn:
399
+ con.execute("BEGIN")
400
+ try:
401
+ retracted = {r[0] for r in con.execute("SELECT edge FROM retractions")}
402
+ self._load_rows(con, s, retracted)
403
+ s.seq = max(self._head_seq(con), s.seq)
404
+ finally:
405
+ if own_txn:
406
+ con.execute("COMMIT")
400
407
  return s
401
408
 
402
409
  def _load_rows(self, con, s: D.Snap, retracted: set, *, shared: bool = False) -> set:
403
410
  """Add every row newer than s.marks to s (all of it for a fresh Snap) and return the ids of the nodes those
404
411
  rows touched. ``shared``: s's per-node lists still belong to an older snapshot too, so a list is copied the
405
- first time this load appends to it (readers of the old snapshot never see it change) and appended to after."""
412
+ first time this load appends to it (readers of the old snapshot never see it change) and appended to after.
413
+
414
+ The tables are read inside one read transaction, so they are read at one moment. Each SELECT used to be its
415
+ own implicit transaction: a second connection (another Graph on the file — hypswarm's scheduler and the MCP
416
+ server a person's agent works through, in one process on two threads) could commit a claim and its
417
+ ``answers`` edge after this load had read ``nodes`` and before it read ``edges``. The edge then named a node
418
+ the snapshot didn't hold yet, was skipped, and its mark was advanced past it, so this Graph never saw the
419
+ edge again until a rebuild: the question stood answered in every other Graph while the scheduler's
420
+ ``check_work`` found "no live claim", reworked the order, and the seat was asked to count again (hypswarm's
421
+ ``test_an_external_seat_works_orders_over_mcp`` on Ubuntu, 2026-11-05 to 11-08). A load already inside a
422
+ transaction (a write that reads) joins it."""
406
423
  touched: set = set()
407
424
  owned: set = set()
408
425
 
@@ -413,6 +430,9 @@ class Graph:
413
430
  else:
414
431
  table[key].append(item)
415
432
 
433
+ own_txn = not con.in_transaction
434
+ if own_txn:
435
+ con.execute("BEGIN")
416
436
  con.row_factory = sqlite3.Row
417
437
  try:
418
438
  m = s.marks
@@ -462,6 +482,8 @@ class Graph:
462
482
  s.leases = {k: v for k, v in s.leases.items() if v["expires"] > t}
463
483
  finally:
464
484
  con.row_factory = None
485
+ if own_txn:
486
+ con.execute("COMMIT")
465
487
  return touched
466
488
 
467
489
  def _delta(self, con, old: D.Snap) -> Optional[D.Snap]:
@@ -469,21 +491,29 @@ class Graph:
469
491
  are shallow copies (C speed), and a per-node list is copied only when this load appends to it — a write
470
492
  costs what it touched, not the size of the graph. Derived standing carries over for every node the write
471
493
  can't have affected (derive.affected_by). None (rebuild instead) when an edge was retracted: retractions
472
- remove, they don't append."""
473
- if con.execute("SELECT 1 FROM events WHERE seq > ? AND kind = 'retract' LIMIT 1", (old.seq,)).fetchone():
474
- return None
475
- s = D.Snap(old.seq, self.policy)
476
- s.nodes = dict(old.nodes)
477
- s.edges = dict(old.edges)
478
- s.out = defaultdict(list, old.out)
479
- s.inn = defaultdict(list, old.inn)
480
- s.withdrawn = dict(old.withdrawn)
481
- s.leases = dict(old.leases)
482
- s.versions = defaultdict(list, old.versions)
483
- s.events = list(old.events)
484
- s.marks = dict(old.marks)
485
- s._sigs = old._sigs # keyed by body: still right for every node not revised
486
- touched = self._load_rows(con, s, set(), shared=True)
494
+ remove, they don't append. The retraction check and the load are one read transaction: a retraction
495
+ committed between them would otherwise be loaded as a live edge and its event's seq passed over."""
496
+ own_txn = not con.in_transaction
497
+ if own_txn:
498
+ con.execute("BEGIN")
499
+ try:
500
+ if con.execute("SELECT 1 FROM events WHERE seq > ? AND kind = 'retract' LIMIT 1", (old.seq,)).fetchone():
501
+ return None
502
+ s = D.Snap(old.seq, self.policy)
503
+ s.nodes = dict(old.nodes)
504
+ s.edges = dict(old.edges)
505
+ s.out = defaultdict(list, old.out)
506
+ s.inn = defaultdict(list, old.inn)
507
+ s.withdrawn = dict(old.withdrawn)
508
+ s.leases = dict(old.leases)
509
+ s.versions = defaultdict(list, old.versions)
510
+ s.events = list(old.events)
511
+ s.marks = dict(old.marks)
512
+ s._sigs = old._sigs # keyed by body: still right for every node not revised
513
+ touched = self._load_rows(con, s, set(), shared=True)
514
+ finally:
515
+ if own_txn:
516
+ con.execute("COMMIT")
487
517
  if old._state and touched:
488
518
  stale = D.affected_by(s, touched)
489
519
  s._state = {k: v for k, v in old._state.items() if k not in stale}
@@ -104,6 +104,11 @@ class Scope:
104
104
  res += typed(out(c, "derived_from"), "result")
105
105
  res += typed(inn(root, "part_of"), "result") + ([t] if n["type"] == "result" else [])
106
106
  self.results = _uniq(res + mine("result"))
107
+ # a run that produced a result on the question is a run on the question. A harness registers run → result
108
+ # → claim with no experiment and no part_of link; the run gate then read "no run executes an experiment" on a
109
+ # measured question whose result and claim were both met, a `run` order went out every round beside the
110
+ # review, and 11 claims on 3 questions settled nothing (BC-250 S3, 2026-11-07)
111
+ self.runs = _uniq(self.runs + [r for x in self.results for r in typed(inn(x, "produced"), "run")])
107
112
  decs = [d for x in self.claims + self.results + self.hypotheses for d in typed(inn(x, "based_on"), "decision")]
108
113
  decs += [t] if n["type"] == "decision" else []
109
114
  self.decisions = _uniq(decs + mine("decision"))
@@ -0,0 +1,76 @@
1
+ """A snapshot is loaded at one moment. Two Graphs on one file — hypswarm's scheduler and the MCP server a person's
2
+ agent works through, one process, two threads — used to tear a load: each table was its own implicit read, so a node
3
+ and its edge committed between the ``nodes`` read and the ``edges`` read left the edge naming a node the snapshot
4
+ didn't hold; the edge was skipped and its mark advanced past it, and that Graph never saw the edge again until a
5
+ rebuild. On Ubuntu CI the scheduler then found "no live claim" on a question every other Graph showed answered,
6
+ reworked the order, and asked the external seat to count again (2026-11-05 to 11-08)."""
7
+ import sqlite3
8
+ import threading
9
+ import time
10
+
11
+ from hypgraph import Graph
12
+
13
+
14
+ class _Interleave:
15
+ """A connection whose first read of ``edges`` lets a hook run first — the window the other Graph writes into."""
16
+
17
+ def __init__(self, con: sqlite3.Connection, hook) -> None:
18
+ object.__setattr__(self, "_con", con)
19
+ object.__setattr__(self, "_hook", hook)
20
+ object.__setattr__(self, "_fired", False)
21
+
22
+ def execute(self, sql, *args):
23
+ if "FROM edges" in sql and not self._fired:
24
+ object.__setattr__(self, "_fired", True)
25
+ self._hook()
26
+ return self._con.execute(sql, *args)
27
+
28
+ def __getattr__(self, name):
29
+ return getattr(self._con, name)
30
+
31
+ def __setattr__(self, name, value):
32
+ setattr(self._con, name, value)
33
+
34
+
35
+ def _produced(g: Graph, run: str) -> list[str]:
36
+ return sorted(e["dst"] for e in g.snapshot().out.get(run, []) if e["rel"] == "produced")
37
+
38
+
39
+ def test_a_row_committed_during_a_load_is_seen_by_the_next_one(tmp_path):
40
+ db = tmp_path / "g.hypgraph"
41
+ a, b = Graph(db), Graph(db)
42
+ inv = a.create(author="importer", type="investigation",
43
+ fields={"title": "Two readers", "objective": "A load must not lose what lands beside it."})["id"]
44
+ run = a.create(author="importer", type="run", inv=inv, fields={"kind": "agent", "state": "succeeded"})["id"]
45
+ a.snapshot() # a holds a snapshot: the next load is a delta
46
+ b.create(author="importer", type="source", inv=inv, fields={"citation": "Someone, a paper, 2026."})
47
+ # # b changed the file: a's next snapshot() loads
48
+
49
+ def write_from_b():
50
+ b.create(author="ana", type="result", inv=inv, links=[{"rel": "produced", "from": run}],
51
+ fields={"summary": "Counted the items.", "kind": "computed", "values": {"m": {"k": 40, "n": 90}}})
52
+
53
+ t = threading.Thread(target=write_from_b)
54
+
55
+ def hook():
56
+ t.start() # b commits a result and its edge — now, if the
57
+ time.sleep(0.3) # load lets it; after the load, if it doesn't
58
+
59
+ a._local.con = _Interleave(a._connect(), hook)
60
+ a.snapshot() # the load with the window in it
61
+ t.join(timeout=15)
62
+ assert not t.is_alive()
63
+ assert not a._connect().in_transaction # the load left no transaction open
64
+ assert _produced(a, run) == _produced(Graph(db), run) != [] # a sees the edge a rebuild sees
65
+
66
+
67
+ def test_a_load_inside_a_write_joins_its_transaction(tmp_path):
68
+ g = Graph(tmp_path / "g.hypgraph")
69
+ con = g._connect()
70
+ con.execute("BEGIN IMMEDIATE")
71
+ try:
72
+ g._invalidate()
73
+ g.snapshot() # a rebuild inside an open transaction
74
+ assert con.in_transaction # still ours: the load did not commit it
75
+ finally:
76
+ con.execute("ROLLBACK")
@@ -20,9 +20,9 @@ def _g(profile: str, asks: str | None = "measure"):
20
20
  return g, inv, q
21
21
 
22
22
 
23
- def _measured(g, inv, q):
23
+ def _measured(g, inv, q, part_of=True):
24
24
  run = g.create(author="w", type="run", inv=inv, fields={"kind": "agent", "name": "count", "state": "succeeded", "config": {"source": "print('count')"}},
25
- links=[{"rel": "part_of", "to": q}])["id"]
25
+ links=[{"rel": "part_of", "to": q}] if part_of else [])["id"]
26
26
  rs = g.create(author="w", type="result", inv=inv, fields={"summary": "A is right on 781 of 1000 items.",
27
27
  "values": {"a_correct": {"k": 781, "n": 1000}}, "outcome": "positive"},
28
28
  links=[{"rel": "produced", "from": run}])["id"]
@@ -45,6 +45,20 @@ def test_a_scientific_measure_question_needs_no_hypothesis():
45
45
  assert g.stage(q)["complete"]
46
46
 
47
47
 
48
+ def test_a_question_answered_through_its_result_needs_no_part_of_link_on_the_run():
49
+ """hypswarm's register_run writes run → result → claim and links only the claim to the question. The run gate
50
+ read "no run executes an experiment on Q1" on a question whose result and claim were both met, so the harness
51
+ issued a `run` order every round beside the review and the seats filed 11 claims on 3 questions without
52
+ settling one (BC-250 S3, 2026-11-07). The run behind a result on the question is a run on the question."""
53
+ g, inv, q = _g("scientific@2")
54
+ c = _measured(g, inv, q, part_of=False)
55
+ st = g.stage(q)
56
+ assert st["stage"] == "reviewed", [(gt["gate"], gt["met"], gt["why"]) for gt in st["gates"]]
57
+ assert next(gt for gt in st["gates"] if gt["gate"] == "run_registered")["met"]
58
+ g.review(author="critic", target=c, verdict="checked", method="Recounted the file.", notes="781 of 1000.")
59
+ assert g.stage(q)["complete"]
60
+
61
+
48
62
  def test_without_asks_every_gate_still_applies():
49
63
  g, inv, q = _g("scientific@2", asks=None)
50
64
  assert g.stage(q)["stage"] == "conjectured"
@@ -0,0 +1,51 @@
1
+ """A claim that is a question says nothing. On a BC-250 run of the bug-fix challenge (2026-11-07) a seat wrote the
2
+ question's own text as a claim, "Which tests in test_semver.py fail on the original semver.py, and what causes each
3
+ failure?", linked it as superseding its right answer, and the reviewer checked it: the question settled on a question."""
4
+ from __future__ import annotations
5
+
6
+ import pytest
7
+
8
+ from hypgraph import Graph, GraphError, Policy
9
+
10
+ QUESTION = "Which tests in test_semver.py fail on the original semver.py, and what causes each failure?"
11
+ ANSWER = ("test_minor_numeric, test_prerelease_before_release and test_newest fail on the original semver.py, because "
12
+ "compare() orders numeric parts as strings.")
13
+
14
+
15
+ def _record():
16
+ g = Graph(policy=Policy(reviewers={"critic"}))
17
+ inv = g.create(author="a", type="investigation", fields={
18
+ "title": "semver", "objective": "Fix the version comparison.", "profile": "software@2"})["id"]
19
+ q = g.create(author="a", type="question", inv=inv, fields={"text": QUESTION})["id"]
20
+ run = g.create(author="a", type="run", inv=inv, fields={"kind": "agent", "name": "pytest on the original",
21
+ "state": "succeeded", "config": {"source": "pytest -q _base"}})["id"]
22
+ res = g.create(author="a", type="result", inv=inv, fields={
23
+ "summary": "Three of six tests fail on the original.", "values": {"tests_passed": {"k": 3, "n": 6}}},
24
+ links=[{"rel": "produced", "from": run}])["id"]
25
+ return g, inv, q, res
26
+
27
+
28
+ def _claim(g, inv, q, res, statement):
29
+ return g.create(author="a", type="claim", inv=inv, fields={
30
+ "statement": statement, "scope": "The six tests in test_semver.py, run on the kept copy of semver.py."},
31
+ links=[{"rel": "answers", "to": q}, {"rel": "supports", "from": res, "why": "the run shows which tests fail"}])
32
+
33
+
34
+ @pytest.mark.parametrize("statement", [QUESTION, "Does the original fail test_newest?", "Which tests fail? "])
35
+ def test_a_claim_that_is_a_question_is_refused(statement):
36
+ g, inv, q, res = _record()
37
+ with pytest.raises(GraphError, match="STATEMENT_EMPTY") as e:
38
+ _claim(g, inv, q, res, statement)
39
+ assert "is a question" in str(e.value)
40
+
41
+
42
+ @pytest.mark.parametrize("statement", [ANSWER, f"Which tests fail on the original semver.py? {ANSWER}", "Why? Because compare() orders numeric parts as strings."])
43
+ def test_an_answer_passes_with_or_without_the_question_before_it(statement):
44
+ g, inv, q, res = _record()
45
+ assert _claim(g, inv, q, res, statement)["id"]
46
+
47
+
48
+ def test_the_refusal_names_what_to_write():
49
+ g, inv, q, res = _record()
50
+ with pytest.raises(GraphError, match="write the answer to it"):
51
+ _claim(g, inv, q, res, QUESTION)
@@ -43,18 +43,40 @@ def test_a_number_the_source_doesnt_have_stays_unbound():
43
43
  "The footbridge opened to the public on { date }.",
44
44
  "Model A easy accuracy = A_easy_acc on the items.",
45
45
  "The strategy with fast window {fast_window} returned the most.",
46
+ # BC-250 sweeps (2026-11-06/07): $name, $name$, and a figure's name in the sentence — three of these were checked
47
+ "The mean gain from test 1 to test 2 was $mean_tutored for tutored pupils.",
48
+ "The rule returned less after paying $cost_per_change$ per position change.",
49
+ "The rule returned ${cost} after costs.",
50
+ "The only outcome that differed is steps_k, with a p-value steps_k_p.",
51
+ "The rule achieved a cumulative return before costs of cum_rule_before.",
52
+ "Over the calls of the fixed sample_three, duplicate_count returned a repeated winner.",
53
+ "The claim <claim_id> answers the question about the footbridge.",
46
54
  ])
47
55
  def test_a_statement_with_a_slot_is_refused(statement):
48
56
  g, inv, src = _record()
49
- with pytest.raises(GraphError, match="STATEMENT_EMPTY"):
57
+ with pytest.raises(GraphError, match="STATEMENT_EMPTY") as e:
50
58
  _claim(g, inv, src, statement)
59
+ assert "unfilled slot" in str(e.value)
51
60
 
52
61
 
53
62
  @pytest.mark.parametrize("statement", [
54
63
  "The minutes set the option `retry = max_tries` for the footbridge.",
55
64
  "The function returned `{date}` for the footbridge.",
56
65
  "The footbridge opened in the summer, as the minutes say.",
66
+ "The minutes say steps_k differs between the arms for the footbridge.",
57
67
  ])
58
68
  def test_code_and_plain_sentences_are_not_slots(statement):
59
69
  g, inv, src = _record()
60
70
  assert _claim(g, inv, src, statement)["id"]
71
+
72
+
73
+ @pytest.mark.parametrize("statement", [
74
+ "The out-of-sample return is 17.13 % (oos_return), versus -17.08 % (buy_and_hold_return).",
75
+ "oos_return = 0.1713 and bh_return: -0.1708 out of sample.",
76
+ "A mean_diff of 1.7 points, with a p_value of 0.33.",
77
+ "The bridge cost $4,210 according to the minutes.",
78
+ "steps_k_p is 0.012 and steps_k_diff was -0.8 steps.",
79
+ ])
80
+ def test_a_name_beside_its_figure_is_not_a_slot(statement):
81
+ from hypgraph.checks import _SLOT
82
+ assert not _SLOT.search(statement), _SLOT.search(statement)
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