hypgraph 0.5.2__tar.gz → 0.6.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.5.2 → hypgraph-0.6.0}/CHANGELOG.md +100 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/PKG-INFO +17 -7
- {hypgraph-0.5.2 → hypgraph-0.6.0}/README.md +16 -6
- hypgraph-0.6.0/docs/ui-plan.md +128 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/__init__.py +1 -1
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/bundle.py +41 -4
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/cli.py +18 -5
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/derive.py +81 -1
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/graph.py +4 -3
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/profiles.py +43 -4
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/server.py +130 -8
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/skill/SKILL.md +1 -1
- hypgraph-0.6.0/src/hypgraph/ui/__init__.py +8 -0
- hypgraph-0.6.0/src/hypgraph/ui/atlas.css +2 -0
- hypgraph-0.6.0/src/hypgraph/ui/atlas.js +7 -0
- hypgraph-0.6.0/src/hypgraph/ui/hypgraph.css +221 -0
- hypgraph-0.6.0/src/hypgraph/ui/hypgraph.js +1068 -0
- hypgraph-0.6.0/src/hypgraph/ui/index.html +29 -0
- hypgraph-0.6.0/tests/test_bundle_telemetry.py +62 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/tests/test_surfaces.py +5 -2
- hypgraph-0.6.0/tests/test_telemetry_stream.py +92 -0
- hypgraph-0.6.0/tests/test_ui_app.py +65 -0
- hypgraph-0.6.0/tests/test_ui_engine.py +133 -0
- hypgraph-0.6.0/tests/test_unnamed_inputs.py +94 -0
- hypgraph-0.5.2/src/hypgraph/ui/__init__.py +0 -7
- hypgraph-0.5.2/src/hypgraph/ui/atlas.css +0 -219
- hypgraph-0.5.2/src/hypgraph/ui/atlas.js +0 -1074
- hypgraph-0.5.2/src/hypgraph/ui/index.html +0 -30
- {hypgraph-0.5.2 → hypgraph-0.6.0}/.gitignore +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/AGENTS.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/CITATION.cff +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/LICENSE +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/NOTICE +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/docs/PLAN.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/docs/decisions/ADR-002-governance-profiles.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/docs/profile-workflows-plan.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/docs/review-2026-10-24.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/docs/spec.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/docs/workflows/README.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/docs/workflows/knowledge.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/docs/workflows/optimization.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/docs/workflows/scientific.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/docs/workflows/software.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/docs/workflows/trading.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/pyproject.toml +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/__main__.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/checks.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/coord.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/demos/__init__.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/demos/broad_street.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/language.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/mcp.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/plan.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/policy.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/refusals.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/report_check.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/schema.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/stats.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/store.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/ui/hypgraph.svg +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/ui/vendor/cose-base.js +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/ui/vendor/layout-base.js +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/src/hypgraph/workflow.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/studies/harmony/tests/test_suite.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/tests/test_cancel_releases_lease.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/tests/test_coord.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/tests/test_d10.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/tests/test_engine.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/tests/test_engine_fixes.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/tests/test_incremental.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/tests/test_log_and_check.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/tests/test_order_detail.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/tests/test_profiles.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/tests/test_profiles_041.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/tests/test_questions_not_assertions.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/tests/test_refusals.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/tests/test_report_answers.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/tests/test_server_auth.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/tests/test_stats_values.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/tests/test_vocabulary_friction.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.6.0}/tests/test_workflows.py +0 -0
|
@@ -1,5 +1,105 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0 — 2026-10-26: the HypGraph UI, live swarm observability, an example per profile
|
|
4
|
+
|
|
5
|
+
The Atlas becomes **HypGraph**: one app for every profile, in each profile's own words, that watches a swarm
|
|
6
|
+
work as it happens and replays any finished run. The design record is `docs/ui-plan.md`. Nothing in the record
|
|
7
|
+
format changes; bundles gain optional fields that sit outside the hash chain.
|
|
8
|
+
|
|
9
|
+
**The HypGraph UI**, which replaces the Atlas:
|
|
10
|
+
- **Eight views in each profile's own words:** Overview, Live, Pipeline, Graph, Evidence, Work orders, Audit log and
|
|
11
|
+
Report. A software investigation shows Requirements and Verifications; a trading one shows Theses and Positions.
|
|
12
|
+
- **Overview** has KPI tiles, the completion criteria, each question's stage, a "needs attention" list and recent
|
|
13
|
+
activity. Attention covers stale evidence, contested claims, premises that fell, leases held past their time,
|
|
14
|
+
returned orders, overclaims refused and a broken chain.
|
|
15
|
+
- **Pipeline** is a swimlane of the profile's stages, with each gate met or not, its evidence, and what would meet it.
|
|
16
|
+
- **Live** shows seats, the record growing and the event stream. It follows `/api/graph/stream` and falls back to
|
|
17
|
+
polling on hosts without it. Any run plays back with play, pause, scrub and speed controls.
|
|
18
|
+
- **Drawers:** a node's drawer gives its standing, fields, links both ways, provenance and what would be at risk if
|
|
19
|
+
it fell. A question's drawer gives its stage and gates.
|
|
20
|
+
- **Enterprise:**
|
|
21
|
+
- light by default, with dark one key away;
|
|
22
|
+
- WCAG AA text contrast in both themes, checked by a test;
|
|
23
|
+
- every screen is a URL, and Back and Forward work;
|
|
24
|
+
- ⌘K search over views, investigations and nodes, plus keys 1–8, `/`, `t` and Esc;
|
|
25
|
+
- CSV export for tables, PNG and JSON for the graph, Markdown and print-to-PDF for the report, and the bundle;
|
|
26
|
+
- the hash-chain badge, read-only, with no build step.
|
|
27
|
+
- **Compatibility for one release:** `atlas.js` and `atlas.css` load the new app, `HypAtlas` is an alias of
|
|
28
|
+
`HypGraph`, and the `--hg-*` theme tokens still apply. Routes a host doesn't serve are hidden rather than failing,
|
|
29
|
+
so PegasusDMZ's older API still works.
|
|
30
|
+
- `dev/ui_smoke.py` renders every view of five profile fixtures in both themes, plus an Atlas-style page, in headless
|
|
31
|
+
Chrome. CI runs it.
|
|
32
|
+
|
|
33
|
+
**Live observability for a running swarm** (with hypswarm ≥ 0.1.0a7's telemetry):
|
|
34
|
+
- **`hypgraph serve` streams a swarm's telemetry.** hypswarm writes `<record>.telemetry.jsonl` beside the record: every
|
|
35
|
+
model call, tool call, work order and scheduler decision, as spans. The server serves it at `GET /api/graph/telemetry`
|
|
36
|
+
(paged by byte offset) and tails it into `/api/graph/stream` as `telemetry` events. Ids become `<seq>:<offset>` so a
|
|
37
|
+
reconnect resumes both. A sidecar that appears after the server starts is picked up, and `--telemetry FILE` /
|
|
38
|
+
`--no-telemetry` override the default. Telemetry is shown, never trusted for standing.
|
|
39
|
+
- **The Live view shows the swarm working:**
|
|
40
|
+
- each seat's call in flight (the model it's calling or the tool it's running, with seconds elapsed), and its calls,
|
|
41
|
+
tokens, cost and errors;
|
|
42
|
+
- KPI tiles for model calls, tokens in → out and cached, spend, tool calls, errors, and budget used against the run's cap;
|
|
43
|
+
- an event stream merging the record and telemetry, with a filter;
|
|
44
|
+
- a trace for any work order, showing each model and tool call on a timeline;
|
|
45
|
+
- alerts: a tool failing three times in a row, a seat repeating an identical tool call, failed model calls, the
|
|
46
|
+
budget passing 80 %;
|
|
47
|
+
- status from the run's own start and end ("Run ended · settled").
|
|
48
|
+
|
|
49
|
+
A record opened before its swarm starts picks the investigation up when it begins. Replay scopes telemetry to the
|
|
50
|
+
replay position. Overview's spend falls back to telemetry when the ledger isn't metered.
|
|
51
|
+
- `dev/ui-fixtures/swarm.*` holds a real scripted swarm's bundle and telemetry, and the smoke check requires its trace.
|
|
52
|
+
|
|
53
|
+
**What the UI needs from the engine:**
|
|
54
|
+
- **Each profile speaks its own words.** `Profile.labels` gives the short noun a screen shows for each node type: a
|
|
55
|
+
software question is a **Requirement**, a trading hypothesis a **Thesis**, a decision a **Position**, an
|
|
56
|
+
optimization result a **Solution**. `profiles.view(pid)` returns what a UI needs: the labels for every node type
|
|
57
|
+
(the kernel's word where the profile has none), the meaning of each type, and the workflow's stages, gates and
|
|
58
|
+
report name. The `@1` profiles are frozen and keep the kernel's words. The labels are display only.
|
|
59
|
+
- **A bundle carries the workflow views.** Each investigation's `derived` now has `profile`, `stages`, `completion`,
|
|
60
|
+
`board` and `orders` beside the report, so a viewer shows stages and gates without the engine. As before, derived
|
|
61
|
+
views are outside the hash chain and never trusted.
|
|
62
|
+
- **Watch a record live.** `GET /api/graph/stream?inv=&after=` pushes every event as it is written, as Server-Sent
|
|
63
|
+
Events. It includes writes from another process on the same file, so a swarm running against the record is
|
|
64
|
+
watched as it works. Ids are event seqs, so a client resumes with `Last-Event-ID`. A comment line every 15 s keeps
|
|
65
|
+
proxies open, and `max_seconds` bounds a read.
|
|
66
|
+
- `GET /api/graph/profile?inv=` gives the investigation's profile as a UI speaks it.
|
|
67
|
+
- "Atlas" is now the HypGraph UI in the server, the CLI, the README and the skill.
|
|
68
|
+
|
|
69
|
+
**A live example per profile on hypgraph.com:**
|
|
70
|
+
- **Five live examples.** `/examples/` has one page per `@2` profile: scientific, software, trading, knowledge and
|
|
71
|
+
optimization. Each is a HypSwarm run on a real record that opens on its live replay. You see the seats take work
|
|
72
|
+
orders, model and tool calls in flight, writes refused and rewritten, and reviews landing, then you can explore the
|
|
73
|
+
pipeline, graph, evidence and report. The pages load offline from their bundles and re-verify each hash chain in
|
|
74
|
+
the browser. `dev/site_examples.py` rebuilds them from HypSwarm's scripted examples, pacing each reply 0.35–1.4 s,
|
|
75
|
+
and every page says so.
|
|
76
|
+
- **A bundle can carry the run's telemetry.** `export(inv, telemetry=path)` and `hypgraph export --telemetry [FILE]`
|
|
77
|
+
(default `<db>.telemetry.jsonl`) add a swarm's sidecar for replay. It is outside the hash chain:
|
|
78
|
+
`verify_bundle` and `from_bundle` ignore it, and the UI labels it "not hash-chained". It is off unless asked for,
|
|
79
|
+
because a sidecar names a run's models, seats and spend.
|
|
80
|
+
- **`play` opens on the replay.** The `play` mount option, or `#view=live&play=1`, opens the Live view and plays the
|
|
81
|
+
run from its first event. With reduced motion requested, it shows the finished run with Play one click away.
|
|
82
|
+
- **A refused write is not an error.** In the Live view, a tool call the record refused counts as "refused by the
|
|
83
|
+
record", with its reason: amber on the seat, in the stream and in the trace. Errors count real failures only.
|
|
84
|
+
- **The site's Atlas page is replaced.** The UI is served from `/app/`, `/explore/` opens any bundle full screen, and
|
|
85
|
+
`/atlas/` forwards there (keeping `?bundle=`, the hand-over from `/verify/` and the fragment). The home page embeds
|
|
86
|
+
the app directly. `/verify/` says when a bundle carries telemetry and opens bundles in HypGraph.
|
|
87
|
+
- **A narrow layout.** Below 760 px of its own width (a phone, or a narrow frame), the app's sidebar becomes a strip of
|
|
88
|
+
tabs and the top bar wraps. It uses a container query, so an embed sizes it, not the window.
|
|
89
|
+
- The smoke check also renders the site: each example opens replaying with its chain verified, plus the home page,
|
|
90
|
+
the explorer, the `/atlas/` forward and the gallery (106 pages).
|
|
91
|
+
|
|
92
|
+
## 0.5.3 — 2026-10-26: results that name no file can go stale
|
|
93
|
+
|
|
94
|
+
- **A result that names no file it read can go stale.** Under a workflow (`@2` profiles), a result whose run has no
|
|
95
|
+
`uses` link was fresh forever, however the files changed. It now goes STALE once an input file of its
|
|
96
|
+
investigation is replaced or withdrawn after the result was written ("it names no file it read, and A1 changed
|
|
97
|
+
after it was written"), and claims resting only on it with it. A file registered on another question only
|
|
98
|
+
(`part_of`) doesn't count against it; run outputs and page excerpts never do; a result written after the change
|
|
99
|
+
is fresh; a result that names its files keeps the usual rule (only those files count). Ordered by the event log,
|
|
100
|
+
not timestamps. Records without a workflow are unchanged. Measured on 851 results in HypSwarm's stored study
|
|
101
|
+
records: none changes standing.
|
|
102
|
+
|
|
3
103
|
## 0.5.2 — 2026-10-26: freshness for sources, stale evidence in the report
|
|
4
104
|
|
|
5
105
|
For P3 (the change suite and the instruments in HypSwarm), the record had to know when a *page* moved, not only a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hypgraph
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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/
|
|
@@ -152,7 +152,8 @@ record behaves exactly as before.
|
|
|
152
152
|
met, every gate MET / NOT MET with what would meet it); `GET /api/graph/stages`; MCP tool `stage`. A workflow
|
|
153
153
|
widens what puts dependants AT RISK — `uses` (a corrected data file, a superseded diff or instance), `cites` (a
|
|
154
154
|
withdrawn source) — and evidence goes **stale** when the artifact it used is superseded or its `valid_until` has
|
|
155
|
-
passed.
|
|
155
|
+
passed. A result whose run names no file it read goes stale when an input file of its question changes after it:
|
|
156
|
+
nothing shows it read the new version (name the files with `uses` and only those count). An honest "it can't be answered" (a checked `inconclusive` claim), a declined thesis (`no_position`) and a
|
|
156
157
|
verified failure each complete a question the gates alone never would.
|
|
157
158
|
|
|
158
159
|
Profiles are versioned and frozen: a record written under `trading@1` means the same thing in a year, and the `@1`
|
|
@@ -193,9 +194,16 @@ with g.writing_for(task_id): # every event carrie
|
|
|
193
194
|
- **Multi-agent:** give each agent its own author name. Make your critic agents reviewers. Have your job runner
|
|
194
195
|
write as the instrument, so its runs count as recorded, not reported.
|
|
195
196
|
- **Your web framework:** `hypgraph.server.dispatch(graph, method, path, query, body)` is the JSON API, and
|
|
196
|
-
`hypgraph.ui.static_dir()` is the
|
|
197
|
+
`hypgraph.ui.static_dir()` is the HypGraph UI to mount next to it.
|
|
198
|
+
- **Watching it live:** `GET /api/graph/stream?inv=` pushes every event as it is written (Server-Sent Events,
|
|
199
|
+
resumable with `Last-Event-ID`), including writes from another process on the same file, such as a swarm running
|
|
200
|
+
against the record. `GET /api/graph/profile?inv=` gives the investigation's profile as a UI speaks it: each node
|
|
201
|
+
type's label ("Requirement", "Thesis"), the workflow's stages and the report's name. A swarm's telemetry sidecar
|
|
202
|
+
(`<db>.telemetry.jsonl`, written by hypswarm) rides the same stream as `telemetry` events, so the Live view shows every
|
|
203
|
+
model and tool call as it happens.
|
|
197
204
|
- **CI:** `hypgraph export -o record.bundle.json && hypgraph verify record.bundle.json`. The bundle is a single file
|
|
198
|
-
your reviewers or auditors can check with SHA-256, in any language or in the browser.
|
|
205
|
+
your reviewers or auditors can check with SHA-256, in any language or in the browser. `--telemetry` carries the
|
|
206
|
+
swarm's sidecar too, outside the chain, so the bundle replays the run with no server.
|
|
199
207
|
|
|
200
208
|
The full guide is at [hypgraph.com/engineers](https://hypgraph.com/engineers/).
|
|
201
209
|
|
|
@@ -204,11 +212,13 @@ The full guide is at [hypgraph.com/engineers](https://hypgraph.com/engineers/).
|
|
|
204
212
|
```bash
|
|
205
213
|
pip install hypgraph
|
|
206
214
|
hypgraph demo # a complete worked investigation (Broad Street, 1854) built into ./research.hypgraph
|
|
207
|
-
hypgraph serve # explore it in the
|
|
215
|
+
hypgraph serve # explore it in the HypGraph UI: http://127.0.0.1:8765
|
|
208
216
|
hypgraph report # the facts, generated from the record
|
|
209
217
|
```
|
|
210
218
|
|
|
211
|
-
Or
|
|
219
|
+
Or watch a swarm work in each profile, replayed live from its record:
|
|
220
|
+
[hypgraph.com/examples](https://hypgraph.com/examples/). Or try to get a claim past the real engine in your browser:
|
|
221
|
+
[hypgraph.com/play](https://hypgraph.com/play/).
|
|
212
222
|
|
|
213
223
|
## Where it came from
|
|
214
224
|
|
|
@@ -219,7 +229,7 @@ egress, pulled out so anyone can use it. Each rule exists because the agents mad
|
|
|
219
229
|
- a summary whose numbers matched no result;
|
|
220
230
|
- "never" written about something that happened once in a thousand.
|
|
221
231
|
|
|
222
|
-
PegasusDMZ is the reference deployment, and its
|
|
232
|
+
PegasusDMZ is the reference deployment, and its graph UI is built from this package. Against PegasusDMZ's live record
|
|
223
233
|
(1,357 events, 790 nodes), the library derives the same standing for every node as the engine it replaced.
|
|
224
234
|
|
|
225
235
|
## Performance
|
|
@@ -120,7 +120,8 @@ record behaves exactly as before.
|
|
|
120
120
|
met, every gate MET / NOT MET with what would meet it); `GET /api/graph/stages`; MCP tool `stage`. A workflow
|
|
121
121
|
widens what puts dependants AT RISK — `uses` (a corrected data file, a superseded diff or instance), `cites` (a
|
|
122
122
|
withdrawn source) — and evidence goes **stale** when the artifact it used is superseded or its `valid_until` has
|
|
123
|
-
passed.
|
|
123
|
+
passed. A result whose run names no file it read goes stale when an input file of its question changes after it:
|
|
124
|
+
nothing shows it read the new version (name the files with `uses` and only those count). An honest "it can't be answered" (a checked `inconclusive` claim), a declined thesis (`no_position`) and a
|
|
124
125
|
verified failure each complete a question the gates alone never would.
|
|
125
126
|
|
|
126
127
|
Profiles are versioned and frozen: a record written under `trading@1` means the same thing in a year, and the `@1`
|
|
@@ -161,9 +162,16 @@ with g.writing_for(task_id): # every event carrie
|
|
|
161
162
|
- **Multi-agent:** give each agent its own author name. Make your critic agents reviewers. Have your job runner
|
|
162
163
|
write as the instrument, so its runs count as recorded, not reported.
|
|
163
164
|
- **Your web framework:** `hypgraph.server.dispatch(graph, method, path, query, body)` is the JSON API, and
|
|
164
|
-
`hypgraph.ui.static_dir()` is the
|
|
165
|
+
`hypgraph.ui.static_dir()` is the HypGraph UI to mount next to it.
|
|
166
|
+
- **Watching it live:** `GET /api/graph/stream?inv=` pushes every event as it is written (Server-Sent Events,
|
|
167
|
+
resumable with `Last-Event-ID`), including writes from another process on the same file, such as a swarm running
|
|
168
|
+
against the record. `GET /api/graph/profile?inv=` gives the investigation's profile as a UI speaks it: each node
|
|
169
|
+
type's label ("Requirement", "Thesis"), the workflow's stages and the report's name. A swarm's telemetry sidecar
|
|
170
|
+
(`<db>.telemetry.jsonl`, written by hypswarm) rides the same stream as `telemetry` events, so the Live view shows every
|
|
171
|
+
model and tool call as it happens.
|
|
165
172
|
- **CI:** `hypgraph export -o record.bundle.json && hypgraph verify record.bundle.json`. The bundle is a single file
|
|
166
|
-
your reviewers or auditors can check with SHA-256, in any language or in the browser.
|
|
173
|
+
your reviewers or auditors can check with SHA-256, in any language or in the browser. `--telemetry` carries the
|
|
174
|
+
swarm's sidecar too, outside the chain, so the bundle replays the run with no server.
|
|
167
175
|
|
|
168
176
|
The full guide is at [hypgraph.com/engineers](https://hypgraph.com/engineers/).
|
|
169
177
|
|
|
@@ -172,11 +180,13 @@ The full guide is at [hypgraph.com/engineers](https://hypgraph.com/engineers/).
|
|
|
172
180
|
```bash
|
|
173
181
|
pip install hypgraph
|
|
174
182
|
hypgraph demo # a complete worked investigation (Broad Street, 1854) built into ./research.hypgraph
|
|
175
|
-
hypgraph serve # explore it in the
|
|
183
|
+
hypgraph serve # explore it in the HypGraph UI: http://127.0.0.1:8765
|
|
176
184
|
hypgraph report # the facts, generated from the record
|
|
177
185
|
```
|
|
178
186
|
|
|
179
|
-
Or
|
|
187
|
+
Or watch a swarm work in each profile, replayed live from its record:
|
|
188
|
+
[hypgraph.com/examples](https://hypgraph.com/examples/). Or try to get a claim past the real engine in your browser:
|
|
189
|
+
[hypgraph.com/play](https://hypgraph.com/play/).
|
|
180
190
|
|
|
181
191
|
## Where it came from
|
|
182
192
|
|
|
@@ -187,7 +197,7 @@ egress, pulled out so anyone can use it. Each rule exists because the agents mad
|
|
|
187
197
|
- a summary whose numbers matched no result;
|
|
188
198
|
- "never" written about something that happened once in a thousand.
|
|
189
199
|
|
|
190
|
-
PegasusDMZ is the reference deployment, and its
|
|
200
|
+
PegasusDMZ is the reference deployment, and its graph UI is built from this package. Against PegasusDMZ's live record
|
|
191
201
|
(1,357 events, 790 nodes), the library derives the same standing for every node as the engine it replaced.
|
|
192
202
|
|
|
193
203
|
## Performance
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# HypGraph UI — plan (Atlas → HypGraph)
|
|
2
|
+
|
|
3
|
+
*Status (2026-10-26): P1–P4 built and released in hypgraph 0.6.0; P5 (the Pegasus pod) waits for a rebuild. The
|
|
4
|
+
decisions in §6 were taken as proposed: plain "HypGraph", light by default, OpenTelemetry as an optional extra with
|
|
5
|
+
prompts and outputs off. The prototype the plan refers to (`dev/ui-prototype/`, with screenshots) lives on the
|
|
6
|
+
proposal's branch, `ui-hypgraph` (PR #10), not on main.*
|
|
7
|
+
|
|
8
|
+
## 1. Where it stands
|
|
9
|
+
|
|
10
|
+
The explorer shipped as **Atlas** (`src/hypgraph/ui/atlas.js`, `hypgraph serve`, the site's `/atlas/`). It predates the
|
|
11
|
+
profiles-as-workflows release (0.5) and was never updated for it:
|
|
12
|
+
|
|
13
|
+
- **It knows only the scientific method.** Its six views (Graph, Hypotheses, Claims, Provenance, Work, Timeline) are
|
|
14
|
+
built around hypotheses and claims. Nothing in it reads a profile, a workflow, a stage or a gate. A software
|
|
15
|
+
record shows "hypotheses" where the team wrote designs; a trading record has no place for positions; a knowledge
|
|
16
|
+
record's sources and excerpts are just nodes.
|
|
17
|
+
- **It has no notion of a running swarm.** It polls the record's events, so writes appear, but who holds which work
|
|
18
|
+
order, which model call is in flight, what it costs, and whether the run is stuck are not visible.
|
|
19
|
+
- **It looks like a demo.** It has a dark neon theme, one dense canvas and a single investigation at a time.
|
|
20
|
+
|
|
21
|
+
## 2. What it becomes
|
|
22
|
+
|
|
23
|
+
**One product name: HypGraph.** "Atlas" goes everywhere: the page title, `hypgraph serve`'s banner, the site and the
|
|
24
|
+
docs. For one release, `HypAtlas` stays as a global alias and `/atlas/` redirects, so embeds don't break.
|
|
25
|
+
|
|
26
|
+
**It speaks each profile's language.** Every profile gains `labels`, a short noun per node type: a software
|
|
27
|
+
`question` is a **Requirement**, a trading `hypothesis` is a **Thesis**, an optimization `result` is a **Solution**.
|
|
28
|
+
Every view uses them, and the stages, gates and report name come from the profile's workflow. A record with no
|
|
29
|
+
profile falls back to the kernel's words.
|
|
30
|
+
|
|
31
|
+
**Information architecture.** These are the prototype's eight views, all driven by data the engine already derives:
|
|
32
|
+
|
|
33
|
+
| view | what it answers |
|
|
34
|
+
|---|---|
|
|
35
|
+
| **Overview** | Is this done, and is it sound? KPI tiles (settled, checked, stale, contested, work orders, spend), the completion criteria, each question's stage, recent activity |
|
|
36
|
+
| **Live** | What is the swarm doing right now? (§3) |
|
|
37
|
+
| **Pipeline** | Where is each question in the workflow, and what would move it? A swimlane of the profile's stages with each gate MET / NOT MET, its evidence and what would meet it |
|
|
38
|
+
| **Graph** | What rests on what? The graph, with profile labels, checked and stale rings, and click-through to the node |
|
|
39
|
+
| **Evidence** | Every claim, result, source and decision, with its derived standing: checked, stale, contested, at risk, re-run by the instrument |
|
|
40
|
+
| **Work orders** | What was each seat asked to do, its predicates for done, and how it ended |
|
|
41
|
+
| **Audit log** | The append-only event log, with the hash chain re-verified on read (the badge in the top bar) |
|
|
42
|
+
| **Report** | The profile's report (Change report, Desk note, Sourced brief, …), with PDF and bundle export |
|
|
43
|
+
|
|
44
|
+
**Enterprise properties.**
|
|
45
|
+
- **Look:** light theme first, with dark available. Dense, tabular and calm. One accent colour (the brand blue);
|
|
46
|
+
status colour only for status.
|
|
47
|
+
- **Every screen is a URL:** `#view=pipeline&inv=software@2&q=Q1&at=40`.
|
|
48
|
+
- **Trust is visible:** a "chain verified · N events" badge, derived standing everywhere, nothing settable by hand.
|
|
49
|
+
- **Search:** ⌘K across nodes, questions and orders.
|
|
50
|
+
- **Keyboard and accessibility:** keyboard navigation, WCAG-AA contrast in both themes.
|
|
51
|
+
- **Export:** CSV for tables, PNG and JSON for the graph, PDF for the report, a verifiable bundle for the record.
|
|
52
|
+
- **Read-only by default:** the UI never writes. Approvals and human review, when they come, go through the
|
|
53
|
+
record's own API with an author, like every other write.
|
|
54
|
+
- **No build step, no network:** vanilla ES modules plus the vendored Cytoscape, served by `hypgraph serve` or
|
|
55
|
+
opened from a static site.
|
|
56
|
+
- **The CLI is unchanged.**
|
|
57
|
+
|
|
58
|
+
## 3. Live observability for a running swarm
|
|
59
|
+
|
|
60
|
+
The goal is everything a run does, from start to end, visible as it happens and auditable afterwards.
|
|
61
|
+
|
|
62
|
+
**Two streams, one timeline.**
|
|
63
|
+
1. **The record** (truth): every write, review, refusal, lease, supersession and stage change. It is already
|
|
64
|
+
hash-chained. `hypgraph serve` gains `GET /api/graph/stream?inv=` (Server-Sent Events) so the UI is pushed each
|
|
65
|
+
event instead of polling.
|
|
66
|
+
2. **Swarm telemetry** (observation, never trusted for standing): what the record can't see. HypSwarm emits
|
|
67
|
+
structured events:
|
|
68
|
+
- **Run:** start, stop, why it stopped, budget and its warning line.
|
|
69
|
+
- **Seats:** each seat's lifecycle, what it holds and what it is doing now.
|
|
70
|
+
- **Model calls:** model id, tokens in, cached and out, $, latency, retries, errors.
|
|
71
|
+
- **Tool calls:** name, a scrubbed summary of the arguments, duration, ok or error.
|
|
72
|
+
- **Scheduler decisions:** dispatch, rework, escalation, re-verification, refresh, world changes, and report
|
|
73
|
+
assembled or lost.
|
|
74
|
+
|
|
75
|
+
Each event carries `run_id`, `inv`, `seat`, `order`, `span_id` and `parent_span_id`.
|
|
76
|
+
|
|
77
|
+
**Where telemetry goes** (`run(..., telemetry=…)`):
|
|
78
|
+
- **JSONL sidecar** (default, local): `<record>.telemetry.jsonl`. `hypgraph serve` tails it and merges it into the
|
|
79
|
+
stream.
|
|
80
|
+
- **OpenTelemetry** (optional extra `hypswarm[otel]`): traces as run → order → model call / tool call spans; metrics
|
|
81
|
+
for tokens, $, calls and latency per seat and model; logs for the rest. This feeds Datadog, Grafana, Honeycomb and
|
|
82
|
+
Splunk without any HypGraph-specific code.
|
|
83
|
+
- **Scrubbing:** secrets and arguments are scrubbed before they leave the process. Prompts and outputs are off by
|
|
84
|
+
default and can be switched on per run.
|
|
85
|
+
|
|
86
|
+
**The Live view.** The prototype shows its shape on replayed real event logs:
|
|
87
|
+
- **Run header:** running / settled / stopped, elapsed time, events per minute, budget burn ($ and calls against
|
|
88
|
+
the cap, warning line), and play / pause / scrub for replay of finished runs.
|
|
89
|
+
- **Seats:** each seat with its current work order and, with telemetry, its in-flight model or tool call (spinner,
|
|
90
|
+
model, elapsed), writes, cost and errors.
|
|
91
|
+
- **The record, growing:** nodes appear as they're written, with stale rings appearing as files change.
|
|
92
|
+
- **Event stream:** record and telemetry merged, filterable by seat, kind and order.
|
|
93
|
+
- **Alerts:** a seat holding an order past its lease, the same tool failing repeatedly, a loop of identical calls,
|
|
94
|
+
the budget warning line, a refusal burst, stale evidence, a lost report.
|
|
95
|
+
- **Trace:** a waterfall per work order: the order's span with each model and tool call inside, tokens and $ per
|
|
96
|
+
span, and where the time went.
|
|
97
|
+
|
|
98
|
+
A finished run's Live view is the same screen fed from the record plus its telemetry sidecar, so every run can be
|
|
99
|
+
replayed exactly.
|
|
100
|
+
|
|
101
|
+
## 4. The site: a working example per profile
|
|
102
|
+
|
|
103
|
+
Each `@2` profile gets an interactive page, `/examples/<profile>/`, running the new UI read-only on a real record.
|
|
104
|
+
The records come from HypSwarm's five scripted examples: 01 scientific, 02 software, 03 trading, 04 knowledge,
|
|
105
|
+
05 optimization. Each page opens on the **Live replay** of that run, so a visitor watches the swarm work, then can
|
|
106
|
+
explore the Pipeline, Graph, Evidence and Report.
|
|
107
|
+
|
|
108
|
+
This needs the bundle to carry what the UI shows. `derived` gains `profile` (labels, stages, report name), `stages`,
|
|
109
|
+
`completion`, `board`, `orders` and the `report` markdown, plus the telemetry sidecar when one exists. As now, the
|
|
110
|
+
bundle's hash chain is re-verified in the browser, and its derived views are shown as derived and never trusted.
|
|
111
|
+
|
|
112
|
+
## 5. Phases
|
|
113
|
+
|
|
114
|
+
| phase | what | verified by |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| **P0** | this plan and the prototype (real data, eight views, deep links, light and dark) | screenshots in `dev/ui-prototype/shots/` |
|
|
117
|
+
| **P1 · engine** | `Profile.labels` for the five profiles; the bundle's `derived` gains profile, stages, completion, board, orders and report; `GET /api/graph/stream` (SSE); "Atlas" renamed in server, CLI and docs | tests; a bundle round-trip; `hypgraph serve` streaming a write |
|
|
118
|
+
| **P2 · UI** | `src/hypgraph/ui/` rebuilt as the HypGraph app (ES modules, no build), all eight views, ⌘K, exports, accessibility; `HypAtlas` alias and `/atlas` redirect | headless screenshots of every view × profile × theme in CI; keyboard and contrast checks |
|
|
119
|
+
| **P3 · telemetry** | HypSwarm's event schema, JSONL sink and optional OTLP exporter; serve merges telemetry into the stream; Live view on a real running swarm, with alerts and the trace waterfall | an end-to-end test: a scripted swarm against a served record, with the UI updating as it runs |
|
|
120
|
+
| **P4 · site** | five profile example pages with Live replay; the site's Atlas page replaced; docs | the pages load offline from the bundle; the chain verifies in the browser |
|
|
121
|
+
| **P5 · pod** | Pegasus's `/atlas` moves to the new UI (needs a pod rebuild: your go-ahead) | the pod's UI smoke check |
|
|
122
|
+
|
|
123
|
+
## 6. Decisions (taken as proposed)
|
|
124
|
+
|
|
125
|
+
1. **Name in the UI:** "HypGraph" as the plain product name (prototype), or a qualifier ("HypGraph Console" /
|
|
126
|
+
"HypGraph Explorer")?
|
|
127
|
+
2. **Theme:** light by default for enterprise (prototype), with dark one click away?
|
|
128
|
+
3. **Telemetry:** OpenTelemetry as an optional extra (proposed), and prompts/outputs off by default?
|
|
@@ -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.6.0"
|
|
20
20
|
|
|
21
21
|
from .bundle import verify_bundle # noqa: E402
|
|
22
22
|
from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
|
|
@@ -11,17 +11,25 @@ SHA-256 over strings plus a few lookups — any language can do it (the site doe
|
|
|
11
11
|
"versions": [{"node", "v", "at", "by", "reason", "body"}], # body = the stored canonical string
|
|
12
12
|
"edges": [{"id", "src", "src_v", "rel", "dst", "dst_v", "data", "at", "by", "why"}], # data likewise
|
|
13
13
|
"withdrawals": [...], "retractions": [...], "leases": [...],
|
|
14
|
-
"derived": {...}
|
|
14
|
+
"derived": {...}, # standing at export time, for viewers; never trusted
|
|
15
|
+
"telemetry": {"source", "covered_by_chain": false, "events": [...]}} # optional: a swarm's telemetry sidecar
|
|
15
16
|
|
|
16
17
|
A scoped bundle (one investigation out of a shared graph) redacts the events outside it to {seq, hash, prev}: their
|
|
17
18
|
content can't be checked, so a change hidden among them (a withdrawal, say) would not show. ``verify_bundle`` says so
|
|
18
19
|
(``complete: false``). To publish a study, keep it in a graph of its own and export all of it.
|
|
20
|
+
|
|
21
|
+
``export(..., telemetry=path)`` adds a swarm's telemetry sidecar (HypSwarm writes ``<record>.telemetry.jsonl``): its
|
|
22
|
+
model and tool calls, work orders and scheduler decisions, so a viewer can replay the run. Telemetry is observation:
|
|
23
|
+
it is not part of the hash chain, ``verify_bundle`` doesn't look at it, and nothing derived from the record rests on
|
|
24
|
+
it. It is off unless asked for — a sidecar names the models, seats and spend of a run.
|
|
19
25
|
"""
|
|
20
26
|
|
|
21
27
|
from __future__ import annotations
|
|
22
28
|
|
|
23
29
|
import hashlib
|
|
24
30
|
import json
|
|
31
|
+
import os
|
|
32
|
+
from pathlib import Path
|
|
25
33
|
from typing import TYPE_CHECKING, Any
|
|
26
34
|
|
|
27
35
|
from . import __version__
|
|
@@ -37,7 +45,23 @@ def _sha(s: str) -> str:
|
|
|
37
45
|
return hashlib.sha256(s.encode()).hexdigest()
|
|
38
46
|
|
|
39
47
|
|
|
40
|
-
def
|
|
48
|
+
def read_sidecar(path: str | os.PathLike, invs: set[str] | None = None) -> list[dict]:
|
|
49
|
+
"""Every complete JSON line of a telemetry sidecar; with ``invs``, those investigations' events (and any without
|
|
50
|
+
one). Lines that aren't JSON objects — a torn last line, say — are skipped."""
|
|
51
|
+
out: list[dict] = []
|
|
52
|
+
with open(path, "rb") as f:
|
|
53
|
+
for line in f.read().splitlines():
|
|
54
|
+
try:
|
|
55
|
+
e = json.loads(line)
|
|
56
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
57
|
+
continue
|
|
58
|
+
if isinstance(e, dict) and (invs is None or e.get("inv") in (None, "") or e.get("inv") in invs):
|
|
59
|
+
out.append(e)
|
|
60
|
+
return out
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def export(g: "Graph", inv: str = "", *, derived: bool = True,
|
|
64
|
+
telemetry: str | os.PathLike | list | None = None) -> dict:
|
|
41
65
|
with g._snap_lock:
|
|
42
66
|
con = g._connect()
|
|
43
67
|
rows = lambda q, *a: [dict(zip([c[0] for c in cur.description], r)) # noqa: E731
|
|
@@ -77,8 +101,15 @@ def export(g: "Graph", inv: str = "", *, derived: bool = True) -> dict:
|
|
|
77
101
|
"events": events, "nodes": nodes, "versions": versions, "edges": edges,
|
|
78
102
|
"withdrawals": withdrawals, "retractions": retractions, "leases": leases,
|
|
79
103
|
}
|
|
104
|
+
invs = [inv] if inv else [n["id"] for n in nodes if n["type"] == "investigation"]
|
|
80
105
|
if derived:
|
|
81
|
-
out["derived"] = derive_views(g,
|
|
106
|
+
out["derived"] = derive_views(g, invs)
|
|
107
|
+
if telemetry is not None:
|
|
108
|
+
if isinstance(telemetry, (str, os.PathLike)):
|
|
109
|
+
src, tel = Path(telemetry).name, read_sidecar(telemetry, set(invs))
|
|
110
|
+
else:
|
|
111
|
+
src, tel = "", [e for e in telemetry if isinstance(e, dict) and e.get("inv") in (None, "", *invs)]
|
|
112
|
+
out["telemetry"] = {"source": src, "covered_by_chain": False, "events": tel}
|
|
82
113
|
return out
|
|
83
114
|
|
|
84
115
|
|
|
@@ -91,9 +122,15 @@ def _event_in(kind: str, ref: str, p: dict, ids: set, eids: set) -> bool:
|
|
|
91
122
|
def derive_views(g: "Graph", invs: list[str]) -> dict:
|
|
92
123
|
"""What a viewer needs without running the engine: standing of every node and the per-investigation views."""
|
|
93
124
|
views = {"investigations": [i for i in g.investigations() if i["id"] in invs], "stats": g.stats(), "by_inv": {}}
|
|
125
|
+
from . import profiles
|
|
94
126
|
for i in invs:
|
|
127
|
+
pid = str((g.get(i).get("body") or {}).get("profile") or "")
|
|
95
128
|
views["by_inv"][i] = {"subgraph": g.subgraph(i), "work": g.work_map(i), "next": g.next_experiments(i, 20),
|
|
96
|
-
"report": g.report(i)
|
|
129
|
+
"report": g.report(i),
|
|
130
|
+
# the workflow views (hypgraph 0.6): a viewer shows each question's stage and gates,
|
|
131
|
+
# the board, the work orders and the profile's own words without the engine
|
|
132
|
+
"profile": profiles.view(pid), "stages": g.stages(i), "completion": g.completion(i),
|
|
133
|
+
"board": g.question_board(i), "orders": g.orders(i)}
|
|
97
134
|
views["verify"] = g.verify()
|
|
98
135
|
return views
|
|
99
136
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
hypgraph — the command line.
|
|
3
3
|
|
|
4
4
|
hypgraph demo build the Broad Street (1854) teaching investigation into ./research.hypgraph
|
|
5
|
-
hypgraph serve open the
|
|
5
|
+
hypgraph serve open the HypGraph UI on http://127.0.0.1:8765
|
|
6
6
|
hypgraph init "Plant growth" --objective "…" --reviewers ada,lin
|
|
7
7
|
hypgraph add hypothesis statement="Blue light grows cress faster." predictions='["taller after 7 days"]'
|
|
8
8
|
hypgraph add result summary="Blue trays averaged 41 mm, red 33 mm." values='{"blue_mm": {"value": 41}}'
|
|
@@ -291,13 +291,20 @@ def cmd_verify(args) -> int:
|
|
|
291
291
|
def cmd_export(args) -> int:
|
|
292
292
|
g = _graph(args)
|
|
293
293
|
inv = g.resolve(args.inv) if args.inv else ""
|
|
294
|
-
|
|
294
|
+
tel = None
|
|
295
|
+
if args.telemetry:
|
|
296
|
+
tel = Path(g.path + ".telemetry.jsonl") if args.telemetry == "auto" else Path(args.telemetry)
|
|
297
|
+
if not tel.is_file():
|
|
298
|
+
print(f"no telemetry at {tel}", file=sys.stderr)
|
|
299
|
+
return 2
|
|
300
|
+
b = g.export(inv, telemetry=tel)
|
|
295
301
|
data = json.dumps(b, ensure_ascii=False, separators=(",", ":") if args.compact else None,
|
|
296
302
|
indent=None if args.compact else 1)
|
|
297
303
|
if args.output:
|
|
298
304
|
Path(args.output).write_text(data, encoding="utf-8")
|
|
299
305
|
print(f"Wrote {args.output}: {len(b['events'])} events, {len(b['nodes'])} nodes"
|
|
300
|
-
+ ("" if b["complete"] else f" ({b['redacted_events']} events outside the investigation redacted)")
|
|
306
|
+
+ ("" if b["complete"] else f" ({b['redacted_events']} events outside the investigation redacted)")
|
|
307
|
+
+ (f", {len(b['telemetry']['events'])} telemetry events" if tel else ""))
|
|
301
308
|
else:
|
|
302
309
|
print(data)
|
|
303
310
|
return 0
|
|
@@ -418,7 +425,7 @@ def cmd_serve(args) -> int:
|
|
|
418
425
|
if tokens and not args.writable:
|
|
419
426
|
print("note: --token only matters with --writable (reads are open)")
|
|
420
427
|
serve(args.db, host=args.host, port=args.port, writable=args.writable, open_browser=not args.no_browser,
|
|
421
|
-
tokens=tokens or None)
|
|
428
|
+
tokens=tokens or None, telemetry=(args.telemetry or "auto") if not args.no_telemetry else None)
|
|
422
429
|
return 0
|
|
423
430
|
|
|
424
431
|
|
|
@@ -501,6 +508,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
501
508
|
s.add_argument("--inv", default="", help="one investigation (events outside it are redacted)")
|
|
502
509
|
s.add_argument("-o", "--output", default="")
|
|
503
510
|
s.add_argument("--compact", action="store_true")
|
|
511
|
+
s.add_argument("--telemetry", nargs="?", const="auto", default="", metavar="FILE",
|
|
512
|
+
help="carry a swarm's telemetry sidecar for replay (default FILE: <db>.telemetry.jsonl); "
|
|
513
|
+
"outside the hash chain")
|
|
504
514
|
s = cmd("import", cmd_import, "rebuild a graph file from a complete bundle")
|
|
505
515
|
s.add_argument("bundle")
|
|
506
516
|
cmd("demo", cmd_demo, "build the Broad Street (1854) teaching investigation")
|
|
@@ -524,11 +534,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
524
534
|
s.add_argument("--install", action="store_true", help="write .claude/skills/hypgraph/SKILL.md")
|
|
525
535
|
s.add_argument("--user", action="store_true", help="install under your home directory instead of this project")
|
|
526
536
|
s.add_argument("--dir", default="", help="project directory to install into (default: here)")
|
|
527
|
-
s = cmd("serve", cmd_serve, "the
|
|
537
|
+
s = cmd("serve", cmd_serve, "the HypGraph UI and JSON API on localhost")
|
|
528
538
|
s.add_argument("--host", default="127.0.0.1")
|
|
529
539
|
s.add_argument("--port", type=int, default=8765)
|
|
530
540
|
s.add_argument("--writable", action="store_true", help="also accept writes (POST /api/graph/write)")
|
|
531
541
|
s.add_argument("--no-browser", action="store_true")
|
|
542
|
+
s.add_argument("--telemetry", default="", metavar="FILE",
|
|
543
|
+
help="a swarm's telemetry sidecar to stream (default: <db>.telemetry.jsonl, picked up when it appears)")
|
|
544
|
+
s.add_argument("--no-telemetry", action="store_true", help="don't serve or stream telemetry")
|
|
532
545
|
s.add_argument("--token", action="append", metavar="SECRET[=AUTHORS]",
|
|
533
546
|
help="bind writes to a bearer token: SECRET=a,b (those authors) or SECRET=* (any non-reserved); "
|
|
534
547
|
"repeatable; $HYPGRAPH_TOKEN adds one. With tokens, unauthenticated writes are 401.")
|
|
@@ -146,6 +146,14 @@ def risky(s: Snap, x: str) -> tuple:
|
|
|
146
146
|
return RISKY + extra if extra else RISKY
|
|
147
147
|
|
|
148
148
|
|
|
149
|
+
def workflow_profile(s: Snap, x: str) -> bool:
|
|
150
|
+
"""Whether x's investigation runs a profile with a workflow (the @2 profiles): freshness rules that need the files
|
|
151
|
+
to be registered apply only there."""
|
|
152
|
+
inv = s.nodes.get(s.nodes[x]["inv"]) if x in s.nodes else None
|
|
153
|
+
pid = str((inv or {}).get("body", {}).get("profile") or "").lower()
|
|
154
|
+
return bool(pid) and pid in EXTRA_RISKY
|
|
155
|
+
|
|
156
|
+
|
|
149
157
|
def upstream(s: Snap, x: str) -> list[tuple[str, str]]:
|
|
150
158
|
"""What x rests on, one step: premises (requires), derivations, a decision's basis, and the run that produced it.
|
|
151
159
|
Support is counted separately (unsupported): one fallen support among several doesn't break a claim."""
|
|
@@ -169,10 +177,68 @@ def excerpts(s: Snap, sid: str) -> list[str]:
|
|
|
169
177
|
and s.nodes[e["src"]]["type"] == "artifact" and (s.nodes[e["src"]]["body"].get("kind") or "") == "excerpt"]
|
|
170
178
|
|
|
171
179
|
|
|
180
|
+
def _order(s: Snap) -> tuple[dict, dict, dict]:
|
|
181
|
+
"""When each node was created, each edge linked and each node withdrawn, as event sequence numbers (timestamps
|
|
182
|
+
are only to the second). Cached on the snapshot until the next event."""
|
|
183
|
+
cached = getattr(s, "_order_cache", None)
|
|
184
|
+
if cached and cached[0] == s.seq:
|
|
185
|
+
return cached[1]
|
|
186
|
+
made: dict[str, int] = {}
|
|
187
|
+
linked: dict[int, int] = {}
|
|
188
|
+
gone: dict[str, int] = {}
|
|
189
|
+
for e in s.events:
|
|
190
|
+
if e["kind"] == "create":
|
|
191
|
+
made.setdefault(e["ref"], e["seq"])
|
|
192
|
+
elif e["kind"] == "link" and isinstance(e.get("payload"), dict) and e["payload"].get("edge") is not None:
|
|
193
|
+
linked[int(e["payload"]["edge"])] = e["seq"]
|
|
194
|
+
elif e["kind"] == "withdraw":
|
|
195
|
+
gone.setdefault(e["ref"], e["seq"])
|
|
196
|
+
out = (made, linked, gone)
|
|
197
|
+
s._order_cache = (s.seq, out) # type: ignore[attr-defined]
|
|
198
|
+
return out
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _moved_since(s: Snap, rid: str) -> list[str]:
|
|
202
|
+
"""Input files of the result's investigation that were replaced or withdrawn after the result was written. A run
|
|
203
|
+
output (an artifact some run produced) and a page excerpt (a re-fetch supersedes it every time) don't count, and a
|
|
204
|
+
file registered on other questions only (``part_of``) doesn't count against a result on a different one."""
|
|
205
|
+
from .workflow import question_of # workflow imports derive; read lazily
|
|
206
|
+
made, linked, gone = _order(s)
|
|
207
|
+
t = made.get(rid)
|
|
208
|
+
if t is None:
|
|
209
|
+
return []
|
|
210
|
+
inv = s.nodes[rid]["inv"]
|
|
211
|
+
q: set | None = None
|
|
212
|
+
out = []
|
|
213
|
+
for aid, a in s.nodes.items():
|
|
214
|
+
if a["type"] != "artifact" or a["inv"] != inv or (a["body"].get("kind") or "") == "excerpt":
|
|
215
|
+
continue
|
|
216
|
+
if any(e["rel"] == "produced" for e in s.inn.get(aid, [])):
|
|
217
|
+
continue
|
|
218
|
+
on = {e["dst"] for e in s.out.get(aid, []) if e["rel"] == "part_of" and s.nodes[e["dst"]]["type"] == "question"}
|
|
219
|
+
if on:
|
|
220
|
+
if q is None:
|
|
221
|
+
# a result reaches its question through the claims built on it, as often as through its run
|
|
222
|
+
qs = {question_of(s, rid)} | {question_of(s, e["src"]) for e in s.inn.get(rid, [])
|
|
223
|
+
if e["rel"] == "derived_from"}
|
|
224
|
+
qs |= {question_of(s, e["dst"]) for e in s.out.get(rid, []) if e["rel"] == "supports"}
|
|
225
|
+
q = {x for x in qs if x}
|
|
226
|
+
if q and not (q & on):
|
|
227
|
+
continue
|
|
228
|
+
when = [linked.get(e["id"], 0) for e in s.inn.get(aid, []) if e["rel"] == "supersedes"]
|
|
229
|
+
if aid in gone:
|
|
230
|
+
when.append(gone[aid])
|
|
231
|
+
if any(w > t for w in when):
|
|
232
|
+
out.append(a["code"] or aid)
|
|
233
|
+
return out
|
|
234
|
+
|
|
235
|
+
|
|
172
236
|
def stale(s: Snap, nid: str) -> list[str]:
|
|
173
237
|
"""Why a result (or a claim through its results) no longer speaks for what it measured; [] when fresh. A result
|
|
174
238
|
is stale when the run that produced it used an artifact that is now superseded or withdrawn, or when its
|
|
175
|
-
``valid_until`` has passed.
|
|
239
|
+
``valid_until`` has passed. Under a workflow, a result whose run names no file it read (no ``uses``) is stale once
|
|
240
|
+
any input file of its investigation is replaced or withdrawn after it was written: nothing shows it read the
|
|
241
|
+
new one. A source is stale when the host re-fetched its page and the page as it is now no
|
|
176
242
|
longer contains its passage (the live excerpt says ``contains_passage: false``), or every excerpt of it was
|
|
177
243
|
superseded. A claim is stale when it has results or cited sources and every one of them is stale."""
|
|
178
244
|
n = s.nodes[nid]
|
|
@@ -200,6 +266,11 @@ def stale(s: Snap, nid: str) -> list[str]:
|
|
|
200
266
|
a = s.nodes[e["dst"]]
|
|
201
267
|
kind = a["body"].get("kind") or "artifact"
|
|
202
268
|
out.append(f"{kind} {a['code'] or e['dst']} is {lc}")
|
|
269
|
+
named = run and any(e["rel"] == "uses" for e in s.out.get(run, []))
|
|
270
|
+
if not named and workflow_profile(s, nid):
|
|
271
|
+
moved = _moved_since(s, nid)
|
|
272
|
+
if moved:
|
|
273
|
+
out.append(f"it names no file it read, and {', '.join(moved[:3])} changed after it was written")
|
|
203
274
|
vu = str(n["body"].get("valid_until") or "").strip()
|
|
204
275
|
if vu and s.now and s.now[:len(vu)] > vu:
|
|
205
276
|
out.append(f"valid_until {vu} has passed")
|
|
@@ -469,6 +540,15 @@ def affected_by(s: Snap, touched: set) -> set:
|
|
|
469
540
|
invs = {x for x in stack if s.nodes[x]["type"] == "investigation"}
|
|
470
541
|
if invs:
|
|
471
542
|
stack += [i for i, n in s.nodes.items() if n["inv"] in invs]
|
|
543
|
+
# a changed file reaches the results that name no file through no edge (see stale): under a workflow, every such
|
|
544
|
+
# result in the file's investigation may have gone stale
|
|
545
|
+
moved = {s.nodes[x]["inv"] for x in stack if s.nodes[x]["type"] == "artifact"}
|
|
546
|
+
if moved:
|
|
547
|
+
for i, n in s.nodes.items():
|
|
548
|
+
if n["type"] == "result" and n["inv"] in moved and workflow_profile(s, i):
|
|
549
|
+
run = producing_run(s, i)
|
|
550
|
+
if not (run and any(e["rel"] == "uses" for e in s.out.get(run, []))):
|
|
551
|
+
stack.append(i)
|
|
472
552
|
while stack:
|
|
473
553
|
y = stack.pop()
|
|
474
554
|
if y in out:
|