hypgraph 0.8.6__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.
- {hypgraph-0.8.6 → hypgraph-0.8.7}/CHANGELOG.md +20 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/PKG-INFO +1 -1
- {hypgraph-0.8.6 → hypgraph-0.8.7}/docs/PLAN.md +1 -1
- {hypgraph-0.8.6 → hypgraph-0.8.7}/docs/RELEASE-READINESS.md +9 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/__init__.py +1 -1
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/graph.py +49 -19
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/workflow.py +5 -0
- hypgraph-0.8.7/tests/test_consistent_load.py +76 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_measure_questions.py +16 -2
- {hypgraph-0.8.6 → hypgraph-0.8.7}/.gitignore +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/AGENTS.md +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/CITATION.cff +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/DISCLAIMER.md +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/LICENSE +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/NOTICE +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/README.md +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/docs/decisions/ADR-002-governance-profiles.md +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/docs/profile-workflows-plan.md +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/docs/review-2026-10-24.md +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/docs/site-plan.md +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/docs/spec.md +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/docs/ui-plan.md +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/docs/verification.md +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/docs/workflows/README.md +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/docs/workflows/knowledge.md +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/docs/workflows/optimization.md +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/docs/workflows/scientific.md +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/docs/workflows/software.md +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/docs/workflows/trading.md +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/examples/frameworks/README.md +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/pyproject.toml +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/__main__.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/assertions.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/blind.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/bundle.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/checks.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/cli.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/coord.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/demos/__init__.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/demos/broad_street.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/demos/dispute.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/demos/linked.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/derive.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/language.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/mcp.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/plan.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/policy.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/profiles.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/references.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/refusals.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/report_check.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/schema.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/server.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/skill/SKILL.md +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/stats.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/store.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/ui/__init__.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/ui/atlas.css +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/ui/atlas.js +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/ui/hypgraph.css +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/ui/hypgraph.i18n.js +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/ui/hypgraph.js +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/ui/hypgraph.svg +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/ui/index.html +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/ui/vendor/cose-base.js +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/src/hypgraph/ui/vendor/layout-base.js +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_accept_work_kind.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_anchoring.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_assertions.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_blind.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_blind_oracles.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_borrowed_words.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_bound_support.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_bundle_telemetry.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_cancel_releases_lease.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_cited_and_failed_support.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_contradictions.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_coord.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_counts_that_bear.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_d10.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_demo_dispute.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_direction_and_units.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_engine.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_engine_fixes.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_figure_said_twice.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_framework_examples.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_i18n.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_incremental.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_link_translate.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_linked_records.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_log_and_check.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_next_order.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_opposed_answers.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_option_lead.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_order_detail.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_profiles.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_profiles_041.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_quantifiers_and_identity.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_questions_not_assertions.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_quoted_counterexample.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_references.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_refusal_friction.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_refusal_results.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_refusals.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_relation_words.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_report_answers.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_report_significance_denied.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_rerunnable.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_restates_scale.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_server_auth.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_server_hosts.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_shape_refusals.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_statement_is_a_question.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_statement_slots.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_stats_not_backtests.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_stats_values.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_surfaces.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_telemetry_stream.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_trust_boundary.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_typed_audits.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_ui_app.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_ui_engine.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_ui_sources_focus.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_uncorrected_effect.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_unnamed_inputs.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_verify_complete.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_verify_lifecycle.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_verify_scope.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_verify_scoped_views.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_verify_trust.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_verify_views.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_versions_not_figures.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_vocabulary_friction.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_what_a_count_is_of.py +0 -0
- {hypgraph-0.8.6 → hypgraph-0.8.7}/tests/test_workflows.py +0 -0
|
@@ -2,6 +2,26 @@
|
|
|
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
|
+
|
|
5
25
|
## 0.8.6 — 2026-11-07: a placeholder is not a statement
|
|
6
26
|
|
|
7
27
|
- **A figure's name is not the figure.** The slot check (`STATEMENT_EMPTY`) now refuses `$mean_tutored`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hypgraph
|
|
3
|
-
Version: 0.8.
|
|
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.
|
|
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
|
|
@@ -268,6 +268,15 @@ was taken from a changelog without being checked against the code.*
|
|
|
268
268
|
> them checked), while a name beside its value passes. Tests: `tests/test_statement_slots.py`,
|
|
269
269
|
> `tests/test_statement_is_a_question.py`. #115 and #116 are merged with green CI. Released as 0.8.6.
|
|
270
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
|
+
|
|
271
280
|
> **After 0.8.4 (2026-11-06).** Checks only, no change to the bundle format: false refusals found running HypSwarm on a
|
|
272
281
|
> small local model. A report that denies significance is warned, not refused; a named p-value carries the test; a word
|
|
273
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.
|
|
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
|
|
@@ -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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|