hypgraph 0.6.0__tar.gz → 0.7.0__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.6.0 → hypgraph-0.7.0}/.gitignore +2 -2
- {hypgraph-0.6.0 → hypgraph-0.7.0}/CHANGELOG.md +46 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/PKG-INFO +10 -9
- {hypgraph-0.6.0 → hypgraph-0.7.0}/README.md +9 -8
- {hypgraph-0.6.0 → hypgraph-0.7.0}/docs/profile-workflows-plan.md +3 -3
- hypgraph-0.7.0/docs/site-plan.md +116 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/docs/spec.md +1 -1
- {hypgraph-0.6.0 → hypgraph-0.7.0}/docs/workflows/README.md +4 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/__init__.py +1 -1
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/checks.py +29 -3
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/coord.py +12 -4
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/derive.py +69 -16
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/graph.py +2 -9
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/profiles.py +15 -3
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/report_check.py +2 -2
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/schema.py +2 -1
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/ui/hypgraph.js +1 -1
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/workflow.py +23 -5
- {hypgraph-0.6.0 → hypgraph-0.7.0}/tests/test_coord.py +12 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/tests/test_incremental.py +11 -3
- {hypgraph-0.6.0 → hypgraph-0.7.0}/tests/test_log_and_check.py +11 -0
- hypgraph-0.7.0/tests/test_measure_questions.py +82 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/tests/test_profiles.py +4 -0
- hypgraph-0.7.0/tests/test_restates_scale.py +103 -0
- hypgraph-0.7.0/tests/test_versions_not_figures.py +78 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/tests/test_vocabulary_friction.py +2 -2
- hypgraph-0.6.0/studies/harmony/tests/test_suite.py +0 -107
- {hypgraph-0.6.0 → hypgraph-0.7.0}/AGENTS.md +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/CITATION.cff +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/LICENSE +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/NOTICE +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/docs/PLAN.md +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/docs/decisions/ADR-002-governance-profiles.md +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/docs/review-2026-10-24.md +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/docs/ui-plan.md +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/docs/workflows/knowledge.md +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/docs/workflows/optimization.md +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/docs/workflows/scientific.md +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/docs/workflows/software.md +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/docs/workflows/trading.md +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/pyproject.toml +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/__main__.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/bundle.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/cli.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/demos/__init__.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/demos/broad_street.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/language.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/mcp.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/plan.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/policy.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/refusals.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/server.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/skill/SKILL.md +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/stats.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/store.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/ui/__init__.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/ui/atlas.css +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/ui/atlas.js +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/ui/hypgraph.css +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/ui/hypgraph.svg +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/ui/index.html +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/ui/vendor/cose-base.js +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/src/hypgraph/ui/vendor/layout-base.js +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/tests/test_bundle_telemetry.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/tests/test_cancel_releases_lease.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/tests/test_d10.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/tests/test_engine.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/tests/test_engine_fixes.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/tests/test_order_detail.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/tests/test_profiles_041.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/tests/test_questions_not_assertions.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/tests/test_refusals.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/tests/test_report_answers.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/tests/test_server_auth.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/tests/test_stats_values.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/tests/test_surfaces.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/tests/test_telemetry_stream.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/tests/test_ui_app.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/tests/test_ui_engine.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/tests/test_unnamed_inputs.py +0 -0
- {hypgraph-0.6.0 → hypgraph-0.7.0}/tests/test_workflows.py +0 -0
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0 — 2026-10-27: measure questions, and claim checks that know a version from a figure
|
|
4
|
+
|
|
5
|
+
- **Measure questions.** A question may say `asks: "measure"`, and its workflow's `measure_skips` gates then don't
|
|
6
|
+
apply to it, in `Graph.stage()` and as order predicates. Until now every question met every gate of its profile:
|
|
7
|
+
a timing question was reworked for a diff, a count for a preregistered hypothesis, a look-ahead check for a
|
|
8
|
+
backtest. Real-model runs lost most of their calls to those reworks. `profiles.view()` lists the skipped gates.
|
|
9
|
+
- **Claim checks:** dotted versions, inline code and labels (case 8, cases 9–12, #3) aren't figures; a measurement's
|
|
10
|
+
key written in place of its number is refused (KEY_FOR_NUMBER); a statement that says nothing ("Placeholder.")
|
|
11
|
+
is refused (STATEMENT_EMPTY).
|
|
12
|
+
- **A rework's predicates are checked like an order's:** an unknown predicate is refused, naming every predicate the
|
|
13
|
+
target can take. A lead had reworked an order with an invented one (`claim_answers_q3`) that could never be met.
|
|
14
|
+
- **Trading: a statistic about returns isn't a return earned.** Correlations, hit rates and other diagnostics of
|
|
15
|
+
returns no longer need fees and slippage (BACKTEST_WITHOUT_COSTS); a return, P&L or Sharpe still does.
|
|
16
|
+
- **hypgraph.com in the app's design system.** One stylesheet on the HypGraph app's tokens and components, light by
|
|
17
|
+
default, restyles every page. The new homepage:
|
|
18
|
+
- opens on the live app, which can switch between the five workflows, Broad Street and the page's own record;
|
|
19
|
+
- offers the agent and engineer paths, then the facts, what the record refuses, the eight rules and the
|
|
20
|
+
arithmetic of never;
|
|
21
|
+
- ends with the workflows, the deployment options, the page's record and what the record shows and doesn't claim.
|
|
22
|
+
- **The homepage proves its figures with its own HypGraph record** (`dev/site_record.py`, now run at every deploy):
|
|
23
|
+
- The instrument measures each figure: tests passing, runtime dependencies, the example records' chains, the bound
|
|
24
|
+
behind the arithmetic of never, and the refused overclaims.
|
|
25
|
+
- A reviewer re-measures each one another way.
|
|
26
|
+
- The page's text is checked against the record with `hypgraph check`, and a copy error fails the deploy.
|
|
27
|
+
- **The report check no longer flags "not ruled out" as an absence claim.** Exclusion words that the sentence negates
|
|
28
|
+
are skipped, as absolutes already were. "Up to 58,155 … are not ruled out" and "a leak cannot be ruled out" are
|
|
29
|
+
hedges; the engine's own bound note says it in those words.
|
|
30
|
+
|
|
31
|
+
- **Derivation scales with the record.** Working out which earlier results a result repeats (`derive.restates`) took a
|
|
32
|
+
pass over the whole graph for every result, recomputing each one's numbers, so deriving a record grew with the
|
|
33
|
+
square of its results. It now reads a per-snapshot index of each investigation's results by the numbers they carry,
|
|
34
|
+
and a result's numbers are computed once per version, shared across snapshots. On `dev/bench.py`'s record: 10,001
|
|
35
|
+
nodes derive in 0.34 s (was 20.35 s), 4,001 in 0.11 s (was 3.65 s). The write-time duplicate check uses the same
|
|
36
|
+
index.
|
|
37
|
+
- **Two fixes to carried standing.** Both were found by the test at scale.
|
|
38
|
+
- Results written in the same second were ordered by id, so a result written later could count as "earlier" and
|
|
39
|
+
change what an older result repeats, after the older one's standing had been derived and carried forward. Ties now
|
|
40
|
+
go by write order.
|
|
41
|
+
- Revising or withdrawing a result now re-derives the results that repeat its numbers. They reach it through no
|
|
42
|
+
edge, so they used to keep their old standing.
|
|
43
|
+
|
|
44
|
+
The randomized incremental suite now gives results numbers, by several authors, and revises them. It fails on the
|
|
45
|
+
old code (5 of 6 seeds) and passes now.
|
|
46
|
+
- `dev/bench.py`: reads, writes and derivation at a given record size, on your machine. The README's performance
|
|
47
|
+
figures come from it.
|
|
48
|
+
|
|
3
49
|
## 0.6.0 — 2026-10-26: the HypGraph UI, live swarm observability, an example per profile
|
|
4
50
|
|
|
5
51
|
The Atlas becomes **HypGraph**: one app for every profile, in each profile's own words, that watches a swarm
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hypgraph
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
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/
|
|
@@ -222,22 +222,23 @@ Or watch a swarm work in each profile, replayed live from its record:
|
|
|
222
222
|
|
|
223
223
|
## Where it came from
|
|
224
224
|
|
|
225
|
-
HypGraph
|
|
226
|
-
|
|
225
|
+
HypGraph began as the research graph of a team of AI agents running long investigations under governed egress, and
|
|
226
|
+
was pulled out so anyone can use it. Each rule exists because the agents made the mistake behind it:
|
|
227
227
|
- a validation that was blind to the answer;
|
|
228
228
|
- a null model that broke the system's own rules;
|
|
229
229
|
- a summary whose numbers matched no result;
|
|
230
230
|
- "never" written about something that happened once in a thousand.
|
|
231
231
|
|
|
232
|
-
|
|
233
|
-
|
|
232
|
+
That team still runs on it, and its graph UI is built from this package. Against the team's live record (1,357 events,
|
|
233
|
+
790 nodes), the library derives the same standing for every node as the engine it replaced.
|
|
234
234
|
|
|
235
235
|
## Performance
|
|
236
236
|
|
|
237
|
-
The engine is pure Python and stays fast at the sizes research records reach.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
237
|
+
The engine is pure Python and stays fast at the sizes research records reach. On `dev/bench.py`'s record (one
|
|
238
|
+
investigation of questions, results, claims and reviews), at 10,001 nodes deriving every node's standing takes 0.34 s
|
|
239
|
+
on a Windows laptop, reading one node 0.07 ms, and a write about 12 ms, mostly SQLite committing to disk; derivation
|
|
240
|
+
grows in step with the record. A write costs what it touches: cached standing is kept for every node the write can't
|
|
241
|
+
affect, and a randomized suite checks that against a full rebuild. Run `python dev/bench.py` for your machine.
|
|
241
242
|
|
|
242
243
|
## Made by
|
|
243
244
|
|
|
@@ -190,22 +190,23 @@ Or watch a swarm work in each profile, replayed live from its record:
|
|
|
190
190
|
|
|
191
191
|
## Where it came from
|
|
192
192
|
|
|
193
|
-
HypGraph
|
|
194
|
-
|
|
193
|
+
HypGraph began as the research graph of a team of AI agents running long investigations under governed egress, and
|
|
194
|
+
was pulled out so anyone can use it. Each rule exists because the agents made the mistake behind it:
|
|
195
195
|
- a validation that was blind to the answer;
|
|
196
196
|
- a null model that broke the system's own rules;
|
|
197
197
|
- a summary whose numbers matched no result;
|
|
198
198
|
- "never" written about something that happened once in a thousand.
|
|
199
199
|
|
|
200
|
-
|
|
201
|
-
|
|
200
|
+
That team still runs on it, and its graph UI is built from this package. Against the team's live record (1,357 events,
|
|
201
|
+
790 nodes), the library derives the same standing for every node as the engine it replaced.
|
|
202
202
|
|
|
203
203
|
## Performance
|
|
204
204
|
|
|
205
|
-
The engine is pure Python and stays fast at the sizes research records reach.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
205
|
+
The engine is pure Python and stays fast at the sizes research records reach. On `dev/bench.py`'s record (one
|
|
206
|
+
investigation of questions, results, claims and reviews), at 10,001 nodes deriving every node's standing takes 0.34 s
|
|
207
|
+
on a Windows laptop, reading one node 0.07 ms, and a write about 12 ms, mostly SQLite committing to disk; derivation
|
|
208
|
+
grows in step with the record. A write costs what it touches: cached standing is kept for every node the write can't
|
|
209
|
+
affect, and a randomized suite checks that against a full rebuild. Run `python dev/bench.py` for your machine.
|
|
209
210
|
|
|
210
211
|
## Made by
|
|
211
212
|
|
|
@@ -25,8 +25,8 @@ exists only as a paragraph in the profile's notes and as a diagram on hypswarm.c
|
|
|
25
25
|
| suggested seats | the **report**: every profile prints "Facts from the research graph" |
|
|
26
26
|
| | the **order**: HypSwarm issues from the question board, not from the stage a question is in |
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
friction because the models were writing discipline work into scientific slots, and the honesty rules by themselves
|
|
28
|
+
An internal study showed what this costs: most refusals were vocabulary
|
|
29
|
+
friction, because the models were writing discipline work into scientific slots, and the honesty rules by themselves
|
|
30
30
|
did not beat a prompt. What a prompt cannot do is remember that the code changed since the tests ran, that the
|
|
31
31
|
source was retracted, that the regime moved, that the teammate's number is stale — the workflow's *invalidation*
|
|
32
32
|
and *gates*. That is where the record should win, and it is exactly what profiles don't yet encode.
|
|
@@ -95,7 +95,7 @@ instrument, mechanical feasibility recomputation (P3). **P2 built in HypSwarm 0.
|
|
|
95
95
|
orders, review on the node the previous gate produced, the record's word on done; the examples' scripted models walk
|
|
96
96
|
the stages; the discovery stub run through its stages). P2 fed back into hypgraph 0.5.1: honest endings the stages
|
|
97
97
|
lacked (a declined thesis, an inconclusive answer, a verified failure), artifacts that learn their kind on
|
|
98
|
-
re-registration, optimization's `checked` stage. **P3 built (2026-10-26):** the instruments in HypSwarm 0.1.0a5 (world watcher, re-verification, source re-fetch, refresh orders) and hypgraph 0.5.2 (source freshness, fresh-only gates, stale in the report); the change suite
|
|
98
|
+
re-registration, optimization's `checked` stage. **P3 built (2026-10-26):** the instruments in HypSwarm 0.1.0a5 (world watcher, re-verification, source re-fetch, refresh orders) and hypgraph 0.5.2 (source freshness, fresh-only gates, stale in the report); the change suite (an internal study), self-test and stub run at $0, predictions locked; the model run waits for the spend approval.
|
|
99
99
|
|
|
100
100
|
## 4. What stays as it is, on purpose
|
|
101
101
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# hypgraph.com and hypswarm.com — plan
|
|
2
|
+
|
|
3
|
+
*Status (2026-10-27): P0 (this plan, the prototype) and P1 (the derivation fix) done; P2 built — hypgraph.com's
|
|
4
|
+
homepage and stylesheet are the design below, and the homepage's record (`dev/site_record.py`) is built at every
|
|
5
|
+
deploy. The prototype folder was retired once the real page replaced it (its screenshots are in the PR that added
|
|
6
|
+
it). P3 (docs, trust page, refusal catalogue) and P4 (hypswarm.com) are next.*
|
|
7
|
+
|
|
8
|
+
## 1. Where it stands
|
|
9
|
+
|
|
10
|
+
The HypGraph UI (0.6) reads as a product: a verified chain, counts, states, a run replaying. The two sites read as
|
|
11
|
+
landing pages: a dark neon theme, an animated graph behind the hero, a gradient headline and cards of copy. The copy
|
|
12
|
+
itself is honest — a scan of both homepages found no overclaims — so the gap is presentation, not substance. A
|
|
13
|
+
visitor who clicks from the site into an example also crosses from one visual world into another.
|
|
14
|
+
|
|
15
|
+
What enterprise readers trust, in roughly this order: the product shown working, specifics they can check, docs with
|
|
16
|
+
depth, a page saying what the thing guarantees and what it doesn't, and restraint. Decoration and adjectives count
|
|
17
|
+
against a tool whose whole pitch is evidence.
|
|
18
|
+
|
|
19
|
+
## 2. The principle: the site follows the record's rules
|
|
20
|
+
|
|
21
|
+
HypGraph refuses a number no result accounts for and an "always" without the counts. The sites meet the same bar,
|
|
22
|
+
and prove it with a HypGraph record of their own:
|
|
23
|
+
|
|
24
|
+
- **Every figure on the page is a claim in the page's record** (`dev/site_record.py`), bound to the measurement it
|
|
25
|
+
came from. The build's instrument (`lab`) runs the measurements: the test suite, the pyproject, the example
|
|
26
|
+
bundles' hash chains, and the overclaims the page shows, written into a scratch record.
|
|
27
|
+
- **A reviewer re-measures each one another way** (`site-check`): the suite run again and its tests counted by
|
|
28
|
+
collection, the pyproject parsed with `tomllib` instead of a regex, the bundles verified by the UI's JavaScript
|
|
29
|
+
verifier (`dev/js_verify.js`, WebCrypto) instead of the Python one, the overclaims written into a second fresh
|
|
30
|
+
record. Its verdict goes into the record: replicated, checked, or an objection.
|
|
31
|
+
- **The page's text is linted against the record** with `hypgraph check`: with the figures filled in, a number no
|
|
32
|
+
claim or result accounts for, an absolute word with nothing behind it, or a citation the record doesn't hold, is
|
|
33
|
+
an error and fails the build. Words the page quotes as examples of what gets refused are marked `data-quote`; a
|
|
34
|
+
phrase true by the engine's design ("on every read": standing is derived on read) is marked `data-ok="<why>"` and
|
|
35
|
+
listed on the page with its reason.
|
|
36
|
+
- **The record ships with the page**, as a bundle a visitor opens in the app on the page ("This page's own record")
|
|
37
|
+
or verifies themselves.
|
|
38
|
+
|
|
39
|
+
### What building the record found already
|
|
40
|
+
|
|
41
|
+
- **The README's performance sentence doesn't hold** on the record shape `dev/bench.py` builds (new: nothing in the
|
|
42
|
+
repo reproduced the figure before). The profile points to `derive.restates`, which compares each result with every
|
|
43
|
+
other result in its investigation and recomputes each one's numbers every time. That is an engine fix of its own
|
|
44
|
+
(P1), now in its own PR: 10,001 nodes derive in 0.34 s (was 20.35 s), and the README quotes the measured figures.
|
|
45
|
+
The fix also found two bugs in carried standing that the randomized suite had never exercised.
|
|
46
|
+
- The record holds its builders to the same rules: the benchmark's own first writes were refused (a number in a
|
|
47
|
+
claim bound to nothing; a result repeating another's numbers), and the copy check failed the prototype's first
|
|
48
|
+
draft three times ("never set by an agent", "what the record guarantees", and a bare 256 in "SHA-256"). Each was
|
|
49
|
+
fixed: reworded, put in code, or marked true by design with its reason.
|
|
50
|
+
- The derived standing caught a wiring mistake in the builder: the first claims carried `derived_from` but no
|
|
51
|
+
`supports` link, so each read "no live support · unverified report". With the support link, each reads "checked ·
|
|
52
|
+
1 independent line of support · lab-recorded run · no external replication" — which is what the page can honestly
|
|
53
|
+
say, and the page shows it as it is.
|
|
54
|
+
|
|
55
|
+
## 3. One design system, the app's
|
|
56
|
+
|
|
57
|
+
The site takes the app's tokens and components (`src/hypgraph/ui/hypgraph.css`): the same colours, type, cards,
|
|
58
|
+
pills, tables and focus rings; light by default and dark one click away; text at WCAG AA in both themes (the
|
|
59
|
+
existing contrast test extends to the site's tokens); motion only where it carries meaning (a run replaying),
|
|
60
|
+
switched off under reduced motion. One header pattern across hypgraph.com and hypswarm.com, each linking the other.
|
|
61
|
+
|
|
62
|
+
## 4. hypgraph.com
|
|
63
|
+
|
|
64
|
+
Navigation: Product · Workflows · What it refuses · Docs · Trust · GitHub, plus the version and a copyable
|
|
65
|
+
`pip install hypgraph`.
|
|
66
|
+
|
|
67
|
+
The homepage, top to bottom (all in the prototype):
|
|
68
|
+
|
|
69
|
+
| section | shows | backed by |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| Hero | one plain sentence on what it is; the slogan, smaller | — |
|
|
72
|
+
| The product, live | the app replaying a run; a switch between the five workflows and **this page's own record**; the chain verified on load | the example bundles; the page's record |
|
|
73
|
+
| Facts | tests passing, runtime dependencies, bundles verified in your browser (a button runs it) | each a claim in the page's record, with its review; click to open it |
|
|
74
|
+
| What it refuses | three overclaims, the engine's refusal (codes and text) and the rewrite it accepted | generated at build, a claim in the record |
|
|
75
|
+
| This page has a record | the claims with their reviews, the copy check's result, the phrases true by design | the record and `hypgraph check` |
|
|
76
|
+
| Workflows | the five profiles: stages, what each refuses, when done; each with its live example | the profiles (`hypgraph profiles`) |
|
|
77
|
+
| For the people who deploy agents | MCP + skill, Python, UI and API, CI gate, bundles, HypSwarm | the CLI as it is |
|
|
78
|
+
| Trust | what the record shows and what it doesn't claim; local, no account, no network calls of its own; trusted publishing | the spec and the release workflow |
|
|
79
|
+
|
|
80
|
+
Other pages: Docs (getting started, concepts, workflows, CLI, Python and HTTP API, MCP, the spec — generated from
|
|
81
|
+
markdown by `dev/build_site.py`, no new dependencies), Trust (the long form of the section, including the bundle
|
|
82
|
+
threat model: scoped bundles, telemetry outside the chain), the refusal catalogue (every code with a real refusal and
|
|
83
|
+
its fix), and the existing Never, Playground, Verify and Examples pages, restyled.
|
|
84
|
+
|
|
85
|
+
## 5. What we say and what we don't
|
|
86
|
+
|
|
87
|
+
**We say** what the record checks, what it refused, what was measured (with where), and what it shows.
|
|
88
|
+
Something the product doesn't do well yet stays off the sites until it does; it isn't dressed up.
|
|
89
|
+
|
|
90
|
+
**We don't say** anything we haven't shown: that teams coordinated through the record beat a well-prompted single
|
|
91
|
+
agent, or cost less; customer logos, testimonials, "trusted by", ROI figures, adjectives such as "enterprise-grade",
|
|
92
|
+
pricing tiers or "contact sales". A short "what it doesn't claim" list sits on the homepage: it doesn't make a
|
|
93
|
+
claim true, and it doesn't make a model reason better.
|
|
94
|
+
|
|
95
|
+
## 6. hypswarm.com
|
|
96
|
+
|
|
97
|
+
The same system, a sister site. The hero is the Live view of a swarm working. Then a free scripted quickstart; the
|
|
98
|
+
framework bridges named only where tests cover them (LangChain, PydanticAI, CrewAI, Bedrock); telemetry and
|
|
99
|
+
OpenTelemetry; "alpha" kept visible. It gets a record of its own the same way (its test suite, its examples
|
|
100
|
+
settling, the bridges' tests).
|
|
101
|
+
|
|
102
|
+
## 7. Phases
|
|
103
|
+
|
|
104
|
+
| phase | what | verified by |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| **P0** | this plan, the prototype homepage, the page's record (`dev/site_record.py`), `dev/bench.py` | screenshots in #17; the record builds with 0 copy errors |
|
|
107
|
+
| **P1** | the engine fix for `derive.restates`, measured with `dev/bench.py` (its own PR) | a test at scale; the benchmark |
|
|
108
|
+
| **P2** | the shared design system and the new hypgraph.com homepage, with its record built in CI | the smoke check, the contrast test, the copy check failing the build |
|
|
109
|
+
| **P3** | docs, the trust page, the refusal catalogue | the same |
|
|
110
|
+
| **P4** | hypswarm.com on the same system, with its record | the same |
|
|
111
|
+
|
|
112
|
+
## 8. Decisions taken
|
|
113
|
+
|
|
114
|
+
1. No deployment is named on the sites.
|
|
115
|
+
2. Timing stays off the sites until the engine fix lands; nothing the product doesn't do well yet is shown.
|
|
116
|
+
3. The slogan stays as a small tagline under the headline, which says what the product does.
|
|
@@ -231,4 +231,4 @@ the writer's assertion. Codes are matched within the investigation being reporte
|
|
|
231
231
|
| warning | `REPORT_CAUSAL_UNCITED` | causal language with no node cited |
|
|
232
232
|
| note | `REPORT_BOUND` | an absolute backed by a cited count: what the count supports |
|
|
233
233
|
|
|
234
|
-
Calibrated on 24 real write-ups
|
|
234
|
+
Calibrated on 24 real write-ups checked against the live record they report on.
|
|
@@ -15,6 +15,10 @@ Conventions used in every spec:
|
|
|
15
15
|
AT RISK. Today only the scientific set (`requires`, `derived_from`, `based_on`, `works_on`, `rests_on`) does.
|
|
16
16
|
- **Fresh** says when a result still speaks for the thing it measured.
|
|
17
17
|
- **Typed artifacts** are `artifact` / `source` / `decision` nodes with required fields per `kind`, not new types.
|
|
18
|
+
- **A measure question skips what it doesn't need.** A question with `asks: "measure"` (a rate, a count, a timing, a
|
|
19
|
+
check of the data or the code as it is) skips the gates its workflow lists in `measure_skips`, in the stage and
|
|
20
|
+
as an order predicate: scientific skips the hypothesis and preregistration; software the diff and the suite on a
|
|
21
|
+
new head; trading the thesis, preregistration, backtest and position. The run, result, claim and review stay.
|
|
18
22
|
|
|
19
23
|
| spec | method it is taken from | the one gate a prompt can't keep |
|
|
20
24
|
|---|---|---|
|
|
@@ -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.
|
|
19
|
+
__version__ = "0.7.0"
|
|
20
20
|
|
|
21
21
|
from .bundle import verify_bundle # noqa: E402
|
|
22
22
|
from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
|
|
@@ -4,6 +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
8
|
SCOPE_MISSING a finding or exclusion doesn't say under which conditions it holds
|
|
8
9
|
EVIDENCE_MISSING nothing supports it
|
|
9
10
|
MEASUREMENT_MALFORMED a measurement doesn't name its result and field
|
|
@@ -11,6 +12,7 @@ and each refusal does the arithmetic the writer skipped, then offers the honest
|
|
|
11
12
|
MEASUREMENT_MISMATCH the measurement says a different number from its result
|
|
12
13
|
UNBOUND_NUMBER a number in the statement comes from nowhere: not a bound measurement, nothing
|
|
13
14
|
computed from them (difference, ratio, percentage, interval), the scope or a constant
|
|
15
|
+
KEY_FOR_NUMBER the statement names a bound measurement's key (case8_expected) in place of its value
|
|
14
16
|
ABSOLUTE_WITHOUT_COUNT "never" / "always" / "eliminates" / "100%" with no count of 0 or n behind it
|
|
15
17
|
(an absolute over a set the sentence enumerates — "all three subsets: …" — is
|
|
16
18
|
not a generalisation and is left alone)
|
|
@@ -48,6 +50,13 @@ _WORDS = {w: i for i, w in enumerate("two three four five six seven eight nine t
|
|
|
48
50
|
"fifteen sixteen seventeen eighteen nineteen twenty".split(), start=2)}
|
|
49
51
|
_WORDNUM = re.compile(r"\b(" + "|".join(_WORDS) + r")\b", re.I) # 'one' is left out: too common as a pronoun
|
|
50
52
|
_LABEL = re.compile(r"""(['"`‘“])[0-9][0-9 ]{0,30}(['"`’”])""")
|
|
53
|
+
# identifiers that look like numbers but aren't measurements: a version (1.10.0, v2.0.0-rc.1), inline code, a number a
|
|
54
|
+
# label word names ("case 8", "cases 9–12", "test 4", "line 12", "#3"), or one that labels the literal after it ("9 ('')")
|
|
55
|
+
_NOT_A_FIGURE = re.compile(
|
|
56
|
+
r"`[^`\n]*`|(?<![\w.])v?\d+(?:\.\d+){2,}(?:[-+][0-9A-Za-z.]+)?(?![\w])"
|
|
57
|
+
r"|\b(?:cases?|tests?|lines?|steps?|items?|rows?|issues?|sections?|rules?|figures?|tables?|questions?|no\.)\s+"
|
|
58
|
+
r"\d+(?:\s*(?:[–-]|to|and|,)\s*\d+)*\b"
|
|
59
|
+
r"|#\d+\b|(?<![\w.])\d+(?=\s*\(\s*['\"‘“`])", re.I)
|
|
51
60
|
UNDERPOWERED = 0.05 # an absolute needs its 95% bound on the opposite rate at or below this
|
|
52
61
|
_CONVENTIONS = re.compile(r"\b(interval|confidence|CI|bounds?|power|alpha|α|significan\w*|level)\b", re.I)
|
|
53
62
|
|
|
@@ -67,13 +76,15 @@ class Problem:
|
|
|
67
76
|
|
|
68
77
|
|
|
69
78
|
def numbers(text: str) -> list[tuple[float, int, bool, str]]:
|
|
70
|
-
"""(value, decimals printed, is_percent, as printed) for each number in a text.
|
|
79
|
+
"""(value, decimals printed, is_percent, as printed) for each number in a text. Versions and inline code are
|
|
80
|
+
identifiers, not figures, and are skipped."""
|
|
71
81
|
out = []
|
|
72
|
-
|
|
82
|
+
text = _NOT_A_FIGURE.sub(" ", text or "")
|
|
83
|
+
for m in _NUM.finditer(text):
|
|
73
84
|
whole, frac, pct = m.group(1), m.group(2), m.group(3)
|
|
74
85
|
v = float(whole.replace(",", "") + ("." + frac if frac else ""))
|
|
75
86
|
out.append((v, len(frac or ""), bool(pct), m.group(0).strip()))
|
|
76
|
-
for m in _WORDNUM.finditer(text
|
|
87
|
+
for m in _WORDNUM.finditer(text):
|
|
77
88
|
out.append((float(_WORDS[m.group(1).lower()]), 0, False, m.group(0)))
|
|
78
89
|
return out
|
|
79
90
|
|
|
@@ -390,6 +401,10 @@ def _exclusion(s: Snap, stmt: str, kind: str, support: list[str], bound: list) -
|
|
|
390
401
|
"effect", words=words)]
|
|
391
402
|
|
|
392
403
|
|
|
404
|
+
_PLACEHOLDER = re.compile(r"^\W*(?:(?:placeholder|tbd|todo|tba|n/?a|none|wip|xxx|test|claim|statement|pending|"
|
|
405
|
+
r"to be (?:determined|confirmed|added)|lorem ipsum)\W*)+$", re.I)
|
|
406
|
+
|
|
407
|
+
|
|
393
408
|
def claim_issues(s: Snap, body: dict, support: list[str], inv: str) -> list[Problem]:
|
|
394
409
|
"""What's wrong with a claim, each with a code, a message and a fix (empty = it may enter the record).
|
|
395
410
|
``support``: ids of the nodes that support it or that it is derived from."""
|
|
@@ -400,6 +415,10 @@ def claim_issues(s: Snap, body: dict, support: list[str], inv: str) -> list[Prob
|
|
|
400
415
|
if invb.get("strict_claims") is False:
|
|
401
416
|
return []
|
|
402
417
|
problems: list[Problem] = []
|
|
418
|
+
if _PLACEHOLDER.match(stmt) or len(re.findall(r"[^\W\d_]{3,}", stmt)) < 2:
|
|
419
|
+
# a claim written to clear a gate (studies/challenges E2: "Placeholder.", contested and left in the record)
|
|
420
|
+
problems.append(Problem("STATEMENT_EMPTY", "the statement says nothing: write the finding itself — what was "
|
|
421
|
+
"measured, on what, and what it came to"))
|
|
403
422
|
if kind in ("finding", "exclusion") and len(scope.strip()) < 10:
|
|
404
423
|
problems.append(Problem("SCOPE_MISSING", "scope: say under which conditions this holds (the data, method, "
|
|
405
424
|
"sample) — at least a sentence"))
|
|
@@ -427,6 +446,13 @@ def claim_issues(s: Snap, body: dict, support: list[str], inv: str) -> list[Prob
|
|
|
427
446
|
"investigation's constants account for: " + ", ".join(p[3] for p in loose[:8])
|
|
428
447
|
+ " — bind them to result fields (measurements) or move design constants into the "
|
|
429
448
|
"scope", fix="bind each number to a result field", words=[p[3] for p in loose[:8]]))
|
|
449
|
+
keys = {str(m.get("key") or "") for m in body.get("measurements") or [] if isinstance(m, dict)}
|
|
450
|
+
named = [k for k in sorted(keys) if ("_" in k or any(ch.isdigit() for ch in k)) and len(k) >= 4
|
|
451
|
+
and re.search(rf"(?<![\w`]){re.escape(k)}(?![\w`])", stmt)]
|
|
452
|
+
if named:
|
|
453
|
+
problems.append(Problem("KEY_FOR_NUMBER", "the statement names a measurement's key instead of saying its value: "
|
|
454
|
+
+ ", ".join(named[:6]) + " — write the number in the sentence; it's bound to that key, "
|
|
455
|
+
"so the record checks it", fix="replace each key with its number", words=named[:6]))
|
|
430
456
|
problems += _absolutes(clean, scope, body, bound)
|
|
431
457
|
if kind not in ("lead", "convention"):
|
|
432
458
|
problems += _comparisons(s, clean, bound, support)
|
|
@@ -340,6 +340,15 @@ def _done_text(pred: dict, target_type: str = "") -> str:
|
|
|
340
340
|
|
|
341
341
|
|
|
342
342
|
# ── issuing and settling ──────────────────────────────────────────
|
|
343
|
+
def _known_predicates(preds: list[dict]) -> None:
|
|
344
|
+
"""Refuse a predicate the record can't evaluate: an order carrying one can never be met."""
|
|
345
|
+
from .graph import GraphError
|
|
346
|
+
bad = [p["p"] for p in preds if p["p"] not in PREDICATES and p["p"] not in profiles.PRED_DESC]
|
|
347
|
+
if bad:
|
|
348
|
+
raise GraphError(f"unknown predicate(s) {bad}: write each as {{\"p\": \"claim_made\"}}; predicates: "
|
|
349
|
+
+ ", ".join(sorted(set(PREDICATES) | set(profiles.PRED_DESC))))
|
|
350
|
+
|
|
351
|
+
|
|
343
352
|
def issue_work(g: "Graph", *, author: str, inv: str, target: str, assignee: str, objective: str, role: str = "",
|
|
344
353
|
done_when: list | None = None, prediction: str = "", method: str = "", budget: dict | None = None,
|
|
345
354
|
premises: list | None = None, lease_minutes: int = 240, note: str = "") -> dict:
|
|
@@ -367,10 +376,7 @@ def issue_work(g: "Graph", *, author: str, inv: str, target: str, assignee: str,
|
|
|
367
376
|
raise GraphError(f"{lease['agent']} holds {_code(s, t)} until {lease['expires']}")
|
|
368
377
|
preds = [norm_pred(p) for p in (done_when or profiles.default_done(s, inv, s.nodes[t]["type"])
|
|
369
378
|
or DEFAULT_DONE.get(s.nodes[t]["type"], [{"p": "result_exists"}]))]
|
|
370
|
-
|
|
371
|
-
if bad:
|
|
372
|
-
raise GraphError(f"unknown predicate(s) {bad}: write each as {{\"p\": \"claim_made\"}}; predicates: "
|
|
373
|
-
+ ", ".join(PREDICATES))
|
|
379
|
+
_known_predicates(preds)
|
|
374
380
|
if premises is None:
|
|
375
381
|
prem = [a for a, _r, _d in D.ancestors(s, t, limit=40) if s.nodes[a]["type"] not in ("artifact", "task")]
|
|
376
382
|
if s.nodes[t]["type"] == "experiment":
|
|
@@ -476,6 +482,8 @@ def rework(g: "Graph", *, author: str, order: str, missing: str, done_when: list
|
|
|
476
482
|
"unmeetable as tooled. Change it (rework with done_when=…), accept what was delivered with a "
|
|
477
483
|
"reason (accept_work needs every predicate met — so change it first), or cancel_work.")
|
|
478
484
|
done_when = [norm_pred(x) for x in done_when] if done_when else None
|
|
485
|
+
if done_when: # a lead reworked with 'claim_answers_q3', an order nothing could meet (S1)
|
|
486
|
+
_known_predicates(done_when)
|
|
479
487
|
prem = [e["dst"] for e in premises_of(s, order)]
|
|
480
488
|
fields = {"objective": objective or b["objective"], "assignee": b["assignee"], "role": b.get("role", ""),
|
|
481
489
|
"done_when": done_when or b.get("done_when") or [], "prediction": b.get("prediction", ""),
|
|
@@ -41,6 +41,10 @@ class Snap:
|
|
|
41
41
|
self.withdrawn: dict[str, dict] = {}
|
|
42
42
|
self.leases: dict[str, dict] = {}
|
|
43
43
|
self._state: dict[str, dict] = {}
|
|
44
|
+
# result signatures by node (keyed on the body they were taken from, so a later snapshot can share them),
|
|
45
|
+
# and per investigation an index of its live results by the numbers they carry — built on first use
|
|
46
|
+
self._sigs: dict[str, tuple] = {}
|
|
47
|
+
self._results: dict[str, tuple] = {}
|
|
44
48
|
self.sig: Any = None
|
|
45
49
|
self.versions: dict[str, list] = defaultdict(list)
|
|
46
50
|
self.events: list[dict] = []
|
|
@@ -448,29 +452,70 @@ def result_signature(body: dict) -> tuple | None:
|
|
|
448
452
|
return tuple(sorted(nums))
|
|
449
453
|
|
|
450
454
|
|
|
451
|
-
def
|
|
452
|
-
"""
|
|
453
|
-
|
|
455
|
+
def signature_of(s: Snap, nid: str) -> tuple | None:
|
|
456
|
+
"""result_signature of a node's current body, computed once per body. Keyed on the body object itself (held, so
|
|
457
|
+
its identity can't be reused): a revision brings a new body, and so does a write that re-stages one."""
|
|
454
458
|
n = s.nodes[nid]
|
|
459
|
+
hit = s._sigs.get(nid)
|
|
460
|
+
if hit is not None and hit[0] is n["body"]:
|
|
461
|
+
return hit[1]
|
|
455
462
|
sig = result_signature(n["body"])
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
463
|
+
s._sigs[nid] = (n["body"], sig)
|
|
464
|
+
return sig
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def result_index(s: Snap, inv: str) -> tuple[dict, dict]:
|
|
468
|
+
"""The investigation's results: (number → the live results carrying it, result → (created_at, its position in
|
|
469
|
+
the graph)). Built once per snapshot, so checking every result costs what the matches cost, not one pass over
|
|
470
|
+
the graph each. The position is write order, so it breaks ties within a second."""
|
|
471
|
+
idx = s._results.get(inv)
|
|
472
|
+
if idx is None:
|
|
473
|
+
by_num: dict[float, set] = defaultdict(set)
|
|
474
|
+
order: dict[str, tuple] = {}
|
|
475
|
+
for pos, (i, m) in enumerate(s.nodes.items()):
|
|
476
|
+
if m["type"] != "result" or m["inv"] != inv:
|
|
477
|
+
continue
|
|
478
|
+
order[i] = (m["created_at"], pos)
|
|
479
|
+
sig = None if i in s.withdrawn else signature_of(s, i)
|
|
480
|
+
for x in set(sig or ()):
|
|
481
|
+
by_num[x].add(i)
|
|
482
|
+
idx = s._results[inv] = (by_num, order)
|
|
483
|
+
return idx
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def repeating(s: Snap, sig: tuple, inv: str) -> list[str]:
|
|
487
|
+
"""Live results in the investigation whose numbers contain all of ``sig`` (as a multiset), in graph order."""
|
|
488
|
+
by_num, order = result_index(s, inv)
|
|
489
|
+
cands: set | None = None
|
|
490
|
+
for x in set(sig):
|
|
491
|
+
ids = by_num.get(x)
|
|
492
|
+
if not ids:
|
|
493
|
+
return []
|
|
494
|
+
cands = set(ids) if cands is None else cands & ids
|
|
495
|
+
if not cands:
|
|
496
|
+
return []
|
|
459
497
|
out = []
|
|
460
|
-
for i
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (m["created_at"], i) >= (n["created_at"], nid):
|
|
464
|
-
continue
|
|
465
|
-
other = result_signature(m["body"])
|
|
466
|
-
if not other:
|
|
467
|
-
continue
|
|
468
|
-
pool = list(other)
|
|
469
|
-
if all(_take(pool, x) for x in mine):
|
|
498
|
+
for i in sorted(cands or (), key=lambda i: order[i][1]):
|
|
499
|
+
pool = list(signature_of(s, i) or ())
|
|
500
|
+
if all(_take(pool, x) for x in sig):
|
|
470
501
|
out.append(i)
|
|
471
502
|
return out
|
|
472
503
|
|
|
473
504
|
|
|
505
|
+
def restates(s: Snap, nid: str) -> list[str]:
|
|
506
|
+
"""Earlier live results in the same investigation whose numbers this result repeats (the same multiset, or a
|
|
507
|
+
subset of it): one measurement written twice is one line of evidence, however many times it is written.
|
|
508
|
+
Earlier is by created_at, then by write order — never by id, which would let a result written later count as
|
|
509
|
+
earlier and change what an older result restates, after its standing was derived and carried forward."""
|
|
510
|
+
n = s.nodes[nid]
|
|
511
|
+
sig = signature_of(s, nid)
|
|
512
|
+
if not sig:
|
|
513
|
+
return []
|
|
514
|
+
_, order = result_index(s, n["inv"])
|
|
515
|
+
mine = order[nid]
|
|
516
|
+
return [i for i in repeating(s, sig, n["inv"]) if i != nid and order[i] < mine]
|
|
517
|
+
|
|
518
|
+
|
|
474
519
|
def _take(pool: list, x: float) -> bool:
|
|
475
520
|
try:
|
|
476
521
|
pool.remove(x)
|
|
@@ -549,6 +594,14 @@ def affected_by(s: Snap, touched: set) -> set:
|
|
|
549
594
|
run = producing_run(s, i)
|
|
550
595
|
if not (run and any(e["rel"] == "uses" for e in s.out.get(run, []))):
|
|
551
596
|
stack.append(i)
|
|
597
|
+
# a result reaches, through no edge, the results that repeat its numbers (see restates): revising or withdrawing
|
|
598
|
+
# it can change what they restate. Every version's numbers count — the results that repeated the old ones too.
|
|
599
|
+
for x in [x for x in stack if s.nodes[x]["type"] == "result"]:
|
|
600
|
+
by_num, _ = result_index(s, s.nodes[x]["inv"])
|
|
601
|
+
nums = {v for ver in s.versions.get(x, ()) for v in (result_signature(ver["body"]) or ())}
|
|
602
|
+
nums |= set(signature_of(s, x) or ())
|
|
603
|
+
for v in nums:
|
|
604
|
+
stack += [i for i in by_num.get(v, ()) if i != x]
|
|
552
605
|
while stack:
|
|
553
606
|
y = stack.pop()
|
|
554
607
|
if y in out:
|
|
@@ -352,6 +352,7 @@ class Graph:
|
|
|
352
352
|
s.versions = defaultdict(list, old.versions)
|
|
353
353
|
s.events = list(old.events)
|
|
354
354
|
s.marks = dict(old.marks)
|
|
355
|
+
s._sigs = old._sigs # keyed by body: still right for every node not revised
|
|
355
356
|
touched = self._load_rows(con, s, set(), shared=True)
|
|
356
357
|
if old._state and touched:
|
|
357
358
|
stale = D.affected_by(s, touched)
|
|
@@ -750,15 +751,7 @@ class Graph:
|
|
|
750
751
|
sig = D.result_signature(body)
|
|
751
752
|
if not sig:
|
|
752
753
|
return []
|
|
753
|
-
hits = []
|
|
754
|
-
for i, n in s.nodes.items():
|
|
755
|
-
if i == exclude or n["type"] != "result" or n["inv"] != inv or i in s.withdrawn:
|
|
756
|
-
continue
|
|
757
|
-
other = D.result_signature(n["body"])
|
|
758
|
-
if other:
|
|
759
|
-
pool = list(other)
|
|
760
|
-
if all(D._take(pool, x) for x in sig):
|
|
761
|
-
hits.append(i)
|
|
754
|
+
hits = [i for i in D.repeating(s, sig, inv) if i != exclude]
|
|
762
755
|
return sorted(hits, key=lambda i: s.nodes[i]["created_at"])
|
|
763
756
|
|
|
764
757
|
def _with_orders(self, out: dict, author: str, inv: str) -> None:
|