hypgraph 0.7.16__tar.gz → 0.7.18__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. {hypgraph-0.7.16 → hypgraph-0.7.18}/CHANGELOG.md +39 -0
  2. {hypgraph-0.7.16 → hypgraph-0.7.18}/PKG-INFO +1 -1
  3. {hypgraph-0.7.16 → hypgraph-0.7.18}/docs/PLAN.md +1 -1
  4. {hypgraph-0.7.16 → hypgraph-0.7.18}/docs/RELEASE-READINESS.md +23 -0
  5. {hypgraph-0.7.16 → hypgraph-0.7.18}/docs/spec.md +13 -5
  6. {hypgraph-0.7.16 → hypgraph-0.7.18}/docs/verification.md +5 -0
  7. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/__init__.py +1 -1
  8. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/bundle.py +60 -12
  9. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/cli.py +5 -0
  10. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/ui/hypgraph.css +2 -0
  11. hypgraph-0.7.18/src/hypgraph/ui/hypgraph.i18n.js +543 -0
  12. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/ui/hypgraph.js +458 -213
  13. hypgraph-0.7.18/tests/test_i18n.py +183 -0
  14. hypgraph-0.7.18/tests/test_verify_lifecycle.py +195 -0
  15. hypgraph-0.7.18/tests/test_verify_scoped_views.py +198 -0
  16. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_verify_views.py +2 -1
  17. {hypgraph-0.7.16 → hypgraph-0.7.18}/.gitignore +0 -0
  18. {hypgraph-0.7.16 → hypgraph-0.7.18}/AGENTS.md +0 -0
  19. {hypgraph-0.7.16 → hypgraph-0.7.18}/CITATION.cff +0 -0
  20. {hypgraph-0.7.16 → hypgraph-0.7.18}/DISCLAIMER.md +0 -0
  21. {hypgraph-0.7.16 → hypgraph-0.7.18}/LICENSE +0 -0
  22. {hypgraph-0.7.16 → hypgraph-0.7.18}/NOTICE +0 -0
  23. {hypgraph-0.7.16 → hypgraph-0.7.18}/README.md +0 -0
  24. {hypgraph-0.7.16 → hypgraph-0.7.18}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
  25. {hypgraph-0.7.16 → hypgraph-0.7.18}/docs/decisions/ADR-002-governance-profiles.md +0 -0
  26. {hypgraph-0.7.16 → hypgraph-0.7.18}/docs/profile-workflows-plan.md +0 -0
  27. {hypgraph-0.7.16 → hypgraph-0.7.18}/docs/review-2026-10-24.md +0 -0
  28. {hypgraph-0.7.16 → hypgraph-0.7.18}/docs/site-plan.md +0 -0
  29. {hypgraph-0.7.16 → hypgraph-0.7.18}/docs/ui-plan.md +0 -0
  30. {hypgraph-0.7.16 → hypgraph-0.7.18}/docs/workflows/README.md +0 -0
  31. {hypgraph-0.7.16 → hypgraph-0.7.18}/docs/workflows/knowledge.md +0 -0
  32. {hypgraph-0.7.16 → hypgraph-0.7.18}/docs/workflows/optimization.md +0 -0
  33. {hypgraph-0.7.16 → hypgraph-0.7.18}/docs/workflows/scientific.md +0 -0
  34. {hypgraph-0.7.16 → hypgraph-0.7.18}/docs/workflows/software.md +0 -0
  35. {hypgraph-0.7.16 → hypgraph-0.7.18}/docs/workflows/trading.md +0 -0
  36. {hypgraph-0.7.16 → hypgraph-0.7.18}/examples/frameworks/README.md +0 -0
  37. {hypgraph-0.7.16 → hypgraph-0.7.18}/pyproject.toml +0 -0
  38. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/__main__.py +0 -0
  39. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/blind.py +0 -0
  40. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/checks.py +0 -0
  41. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/coord.py +0 -0
  42. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/demos/__init__.py +0 -0
  43. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/demos/broad_street.py +0 -0
  44. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/derive.py +0 -0
  45. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/graph.py +0 -0
  46. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/language.py +0 -0
  47. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/mcp.py +0 -0
  48. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/plan.py +0 -0
  49. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/policy.py +0 -0
  50. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/profiles.py +0 -0
  51. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/references.py +0 -0
  52. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/refusals.py +0 -0
  53. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/report_check.py +0 -0
  54. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/schema.py +0 -0
  55. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/server.py +0 -0
  56. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/skill/SKILL.md +0 -0
  57. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/stats.py +0 -0
  58. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/store.py +0 -0
  59. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/ui/__init__.py +0 -0
  60. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/ui/atlas.css +0 -0
  61. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/ui/atlas.js +0 -0
  62. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/ui/hypgraph.svg +0 -0
  63. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/ui/index.html +0 -0
  64. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
  65. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/ui/vendor/cose-base.js +0 -0
  66. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
  67. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
  68. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/ui/vendor/layout-base.js +0 -0
  69. {hypgraph-0.7.16 → hypgraph-0.7.18}/src/hypgraph/workflow.py +0 -0
  70. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_accept_work_kind.py +0 -0
  71. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_anchoring.py +0 -0
  72. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_blind.py +0 -0
  73. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_blind_oracles.py +0 -0
  74. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_bound_support.py +0 -0
  75. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_bundle_telemetry.py +0 -0
  76. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_cancel_releases_lease.py +0 -0
  77. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_cited_and_failed_support.py +0 -0
  78. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_coord.py +0 -0
  79. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_d10.py +0 -0
  80. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_engine.py +0 -0
  81. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_engine_fixes.py +0 -0
  82. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_framework_examples.py +0 -0
  83. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_incremental.py +0 -0
  84. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_link_translate.py +0 -0
  85. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_log_and_check.py +0 -0
  86. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_measure_questions.py +0 -0
  87. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_opposed_answers.py +0 -0
  88. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_option_lead.py +0 -0
  89. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_order_detail.py +0 -0
  90. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_profiles.py +0 -0
  91. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_profiles_041.py +0 -0
  92. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_questions_not_assertions.py +0 -0
  93. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_references.py +0 -0
  94. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_refusal_friction.py +0 -0
  95. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_refusal_results.py +0 -0
  96. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_refusals.py +0 -0
  97. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_relation_words.py +0 -0
  98. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_report_answers.py +0 -0
  99. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_rerunnable.py +0 -0
  100. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_restates_scale.py +0 -0
  101. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_server_auth.py +0 -0
  102. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_server_hosts.py +0 -0
  103. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_shape_refusals.py +0 -0
  104. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_stats_not_backtests.py +0 -0
  105. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_stats_values.py +0 -0
  106. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_surfaces.py +0 -0
  107. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_telemetry_stream.py +0 -0
  108. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_trust_boundary.py +0 -0
  109. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_ui_app.py +0 -0
  110. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_ui_engine.py +0 -0
  111. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_uncorrected_effect.py +0 -0
  112. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_unnamed_inputs.py +0 -0
  113. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_verify_complete.py +0 -0
  114. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_verify_scope.py +0 -0
  115. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_verify_trust.py +0 -0
  116. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_versions_not_figures.py +0 -0
  117. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_vocabulary_friction.py +0 -0
  118. {hypgraph-0.7.16 → hypgraph-0.7.18}/tests/test_workflows.py +0 -0
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.18 — 2026-10-31: a scoped bundle can't call an active node superseded
4
+
5
+ - **A scoped bundle can't show an active node as superseded and still say its views are checked.** A re-audit of
6
+ 0.7.17 found that a bundle naming a `scope.inv` (which the chain doesn't cover) could show any node, an active claim
7
+ or the investigation itself, as superseded, and verify against its published head with `views: "checked"` and
8
+ coverage complete: the verifier let a scoped bundle call any non-withdrawn node superseded, because links from
9
+ other investigations are left out. Now, in the Python and browser verifiers alike, that allowance applies only
10
+ when events really are redacted (without them every link is in the bundle, so the lifecycle fails) and only to a
11
+ type a `supersedes` or `corrects` link can point at (never an investigation or a review), and even then the views
12
+ are `"unconfirmed"`, not `"checked"`, with a warning naming each node; `hypgraph verify` prints
13
+ `views: NOT CHECKED`. The Explorer header shows a *views not checked* pill for `historical` and `unconfirmed`
14
+ views, and the verify page names both instead of calling them checked (`tests/test_verify_scoped_views.py`).
15
+ - **The spec says what a verifier does with lifecycles.** `docs/spec.md` and `docs/verification.md` no longer say a
16
+ verifier accepts the value either rule gives: the current rule is authoritative, a 0.3-only lifecycle is
17
+ `historical`, a scoped one superseded from outside is `unconfirmed`, anything else fails.
18
+
19
+ ## 0.7.17 — 2026-10-31: a superseded node can't verify as active
20
+
21
+ - **A superseded node can't verify as active.** A pre-launch audit of 0.7.16 found that the verifier accepted a node's
22
+ lifecycle under either the current rule or 0.3's, which counted only an active replacement: in a chain RS3
23
+ supersedes RS2 supersedes RS1, 0.3 calls RS1 active, so a bundle edited to show RS1 active verified with
24
+ `views: "checked"`, its chain, rows and head untouched; setting the bundle's `generator` (which the chain doesn't
25
+ cover) to 0.3 also turned `--standing` from a problem into "not checked". Now, in the Python and browser verifiers
26
+ alike, a lifecycle is checked against the current rule only. One that matches only 0.3's rule still verifies (a 0.3
27
+ export shows it so) but the result says `views: "historical"`, not `"checked"`, and the warning names each such node
28
+ and what the current rule makes it; `hypgraph verify` prints `views: NOT CHECKED`. The generator is never consulted.
29
+ Covered for `supersedes` and `corrects`, chains of three, withdrawn replacements, scoped bundles and a forged or
30
+ deleted generator, with the browser verifier held to the same verdict, warning and coverage
31
+ (`tests/test_verify_lifecycle.py`).
32
+ - **The Explorer and the website speak Spanish, French, German and Simplified Chinese.** The UI's strings are in a
33
+ table (English, the source, in `hypgraph.js`; translations in `hypgraph.i18n.js`, loaded on demand or registered
34
+ with `HypGraph.addStrings`). The language comes from `?lang=`, the host page's `lang`, a saved choice, then the
35
+ browser's, else English; a switcher in the header changes it (`langSwitch: false` hides it in an embed). The main
36
+ site pages are built in each language under `site/<lang>/` by `dev/build_site.py` from `i18n/site/<lang>.json`,
37
+ with `hreflang` alternates, a language menu and a note that the English is authoritative. `i18n/glossary.json`
38
+ fixes how claim, standing, supersedes, withdrawn, head, bundle, scope … are rendered, and the tests hold every
39
+ table to it (`tests/test_i18n.py`). What the verifier, the engine and the CLI say stays English and byte-identical
40
+ in Python and JS: agents and audits parse it.
41
+
3
42
  ## 0.7.16 — 2026-10-31: a bundle's scope must be a real one
4
43
 
5
44
  - **A bundle's scope must be a real one.** A re-audit of 0.7.15 found that `scope.inv`, which the chain doesn't
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hypgraph
3
- Version: 0.7.16
3
+ Version: 0.7.18
4
4
  Summary: The evidence layer for AI research agents: a hash-chained research record that checks every claim as it is written and derives how well each is known.
5
5
  Project-URL: Homepage, https://hypgraph.com
6
6
  Project-URL: Documentation, https://hypgraph.com/engineers/
@@ -109,7 +109,7 @@ claim's derived standing.
109
109
  Multi-writer sync between graphs (bundles cover sharing), authentication (the host's job; the HTTP server binds to
110
110
  loopback and is read-only unless started with `--writable`), and a hosted service.
111
111
 
112
- ## Status at 0.7.16 (2026-10-31)
112
+ ## Status at 0.7.18 (2026-10-31)
113
113
 
114
114
  All seven phases are done: the library, bundles, the server and UI, the MCP server, the website, Pegasus running on
115
115
  the package, and releases to PyPI from tags with trusted publishing. What came after the plan, by release, is in
@@ -47,6 +47,29 @@ was taken from a changelog without being checked against the code.*
47
47
  > **Where that stands.** #72 is merged with green CI (17 of 17 checks); the suite is 412 passed, and hypswarm and
48
48
  > hypmesh pass on it. Released as 0.7.16.
49
49
 
50
+ > **Pre-launch audit of 0.7.16 (2026-10-31).** Confirmed and fixed for 0.7.17: the verifier accepted a lifecycle under
51
+ > either the current rule or 0.3's, so the oldest node of a chain of two or more replacements could be shown active
52
+ > and the bundle verified with `views: "checked"`, chain, rows and head untouched; a `generator` set to 0.3 (not
53
+ > covered by the chain) also turned `--standing` into "not checked". A lifecycle is now checked against the current
54
+ > rule only; one that matches only 0.3's verifies with `views: "historical"` and a warning naming each node, in
55
+ > Python and the browser alike, whatever the generator says. Tests: `tests/test_verify_lifecycle.py` (five fail on
56
+ > the 0.7.16 code). The required checks are now all 17 CI jobs, and the repository has its homepage and topics.
57
+ >
58
+ > **Where that stands.** #75 is merged with green CI (17 of 17 checks); from the 0.7.17 wheel in a fresh venv the
59
+ > suite is 424 passed, and hypswarm (118 passed) and hypmesh (37 passed) pass on it. Released as 0.7.17.
60
+
61
+ > **Re-audit of 0.7.17 (2026-10-31).** Confirmed and fixed for 0.7.18: a bundle naming a `scope.inv` (not covered by
62
+ > the chain) could show any non-withdrawn node, an active claim or the investigation itself, as superseded and verify
63
+ > against its published head with `views: "checked"` and coverage complete; the published Broad Street bundle, which
64
+ > has no redacted events, was enough. The scoped allowance now needs redacted events and a type a `supersedes` /
65
+ > `corrects` link can reach (never an investigation or a review), and even then the views are `"unconfirmed"`, with
66
+ > each node named, in Python and the browser alike. The verify page and the Explorer header now say *views not
67
+ > checked* for `historical` and `unconfirmed` views, and the spec no longer says a verifier accepts either rule.
68
+ > Tests: `tests/test_verify_scoped_views.py`.
69
+ >
70
+ > **Where that stands.** #77 is merged with green CI (17 of 17 checks); from the 0.7.18 wheel in a fresh venv the
71
+ > suite is 431 passed, and hypswarm (118 passed) and hypmesh (37 passed) pass on it. Released as 0.7.18.
72
+
50
73
  ## Recommendation: CONDITIONAL GO
51
74
 
52
75
  HypGraph is ready for a public developer preview once #59, #60 and #61 are merged with green CI and released as one
@@ -153,7 +153,9 @@ out a change hidden among them. `scope.title` is a label; a viewer names the sco
153
153
  **What a verifier vouches for.** `ok` means the chain links and every row and event agree, both ways. `coverage:
154
154
  complete` means every row field was vouched for by its event and nothing is redacted (`partial` otherwise). `views:
155
155
  checked` means every fact the derived views show that the rows fix is present and agrees with the rows, and the
156
- scope is a real one; `views: none` means the bundle carries no views. None of them vouches for standing, the report,
156
+ scope is a real one; `views: none` means the bundle carries no views. `views: historical` and `views: unconfirmed`
157
+ mean the views verify but some lifecycles in them couldn't be checked (below), and the warning names each one: a
158
+ viewer must not present them as checked. None of them vouches for standing, the report,
157
159
  stages or work orders (as exported), for who an author really is, or that the head was published.
158
160
 
159
161
  **Anchoring.** A bundle proves only its own consistency. To prove it wasn't rebuilt from scratch, publish its `head`
@@ -171,10 +173,16 @@ investigation node), which investigations the list shows (each once), which node
171
173
  `by` and `why`. Every one of these must be **present**, not only equal where present: a viewer fills a missing fact
172
174
  with a default, and a withdrawn claim without its lifecycle would read as active. The lifecycle is derived from the
173
175
  verified rows (withdrawn when a withdrawal exists; superseded when a live `supersedes` or `corrects` replacement is
174
- active or, since 0.4, itself superseded; an investigation's own `state` otherwise), and a verifier accepts the value
175
- either rule gives. A scoped bundle leaves out links to other investigations, so it may also call a node superseded
176
- (never a withdrawn node anything but withdrawn). A bundle exported without derived views verifies, and a verifier says
177
- it has none (`views: none`). **Standing** (review state, support, at-risk, the sentence) can't
176
+ active or, since 0.4, itself superseded; an investigation's own `state` otherwise). A verifier checks the
177
+ lifecycle against the current rule. A lifecycle that only 0.3's rule gives (it counted an active replacement only)
178
+ verifies, since a 0.3 export shows it so, but the chain doesn't record which version exported a bundle, so the views
179
+ are `historical`, not `checked`. A scoped bundle with redacted events leaves out links from other investigations, and
180
+ one of them may replace a node: a node shown superseded with no replacement in the rows verifies only if its type can
181
+ be replaced at all (every type but `investigation` and `review`) and it isn't withdrawn, and the views are
182
+ `unconfirmed`, since that replacement is among the redacted events. A bundle with no redacted events holds every link,
183
+ scoped or not, so any other lifecycle the rows don't give fails. `scope` isn't covered by the chain, so naming one
184
+ never loosens a check on its own. A bundle exported without derived views verifies, and a verifier says it has none
185
+ (`views: none`). **Standing** (review state, support, at-risk, the sentence) can't
178
186
  be checked from the rows alone: it is what the exporting engine derived, and so are the report, the workflow stages,
179
187
  completion, the question board and work-order status. A verifier reports them as not checked, or re-derives them with
180
188
  the same engine version (`hypgraph verify FILE --standing`, under the caller's policy with `--policy`) and compares.
@@ -90,6 +90,11 @@ separate implementation.
90
90
  title and lifecycle, each link's ends and fields, the full investigation list — must be present in the derived
91
91
  views, not only right where present: a view that drops a withdrawn claim's lifecycle fails, rather than being shown
92
92
  as active. A bundle exported without derived views (`export(derived=False)`) verifies and says `views: none`.
93
+ - **A lifecycle that can't be checked isn't called checked.** One only hypgraph 0.3's rule gives verifies with
94
+ `views: historical`. In a scoped bundle with redacted events, a node shown superseded with no replacement in the
95
+ rows (a replacement in another investigation would be redacted) verifies with `views: unconfirmed`, and only if its
96
+ type can be superseded at all; investigations and reviews can't. Without redacted events every link is in the
97
+ bundle, so such a node fails. The warning names each node; the Explorer and the verify page say *views not checked*.
93
98
 
94
99
  ## Scoped bundles and references
95
100
 
@@ -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.7.16"
19
+ __version__ = "0.7.18"
20
20
 
21
21
  from .bundle import anchored, verify_bundle # noqa: E402
22
22
  from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
@@ -34,7 +34,7 @@ from pathlib import Path
34
34
  from typing import TYPE_CHECKING, Any
35
35
 
36
36
  from . import __version__
37
- from .schema import SAME_TYPE
37
+ from .schema import RELS, SAME_TYPE
38
38
  from .store import GENESIS, ROW_TABLES, check_rows, event_body, now
39
39
 
40
40
  if TYPE_CHECKING:
@@ -241,13 +241,16 @@ def _verify_bundle(b: dict, policy: dict | None = None) -> dict:
241
241
  problem = _scope_problem(b, tables, red)
242
242
  if problem:
243
243
  return {"ok": False, "problem": problem}
244
- problem = _derived_problem(b, tables)
244
+ historical: list = []
245
+ unconfirmed: list = []
246
+ problem = _derived_problem(b, tables, historical, unconfirmed, red)
245
247
  if problem:
246
248
  return {"ok": False, "problem": f"the derived views contradict the verified record: {problem}"}
247
249
  out = {"ok": True, "checked": checked, "head": prev, "complete": red == 0, "redacted": red,
248
250
  "nodes": len(tables["nodes"]), "edges": len(tables["edges"]), "unchecked": rows["unchecked"],
249
251
  "coverage": "complete" if not rows["unchecked"] and not red else "partial",
250
- "views": "none" if b.get("derived") is None else "checked",
252
+ "views": ("none" if b.get("derived") is None else "historical" if historical
253
+ else "unconfirmed" if unconfirmed else "checked"),
251
254
  "policy": {"trusted": sorted(policy or {}), "importer": importer},
252
255
  "standing": {"checked": False, "note": STANDING_NOTE}}
253
256
  notes = []
@@ -259,11 +262,33 @@ def _verify_bundle(b: dict, policy: dict | None = None) -> dict:
259
262
  if stated != importer:
260
263
  notes.append(f"The bundle names {stated} as its importer; the chain doesn't cover that, so {importer} was "
261
264
  "used.")
265
+ if historical:
266
+ notes.append(_historical_note(historical))
267
+ if unconfirmed:
268
+ notes.append(_unconfirmed_note(unconfirmed))
262
269
  if notes:
263
270
  out["warning"] = " ".join(notes)
264
271
  return out
265
272
 
266
273
 
274
+ def _listed(names: list) -> str:
275
+ return "; ".join(names[:8]) + (f"; and {len(names) - 8} more" if len(names) > 8 else "")
276
+
277
+
278
+ def _historical_note(historical: list) -> str:
279
+ shown = _listed([f"{name} shown {s}, the current rule makes it {w}" for name, s, w in historical])
280
+ return (f"The views are not checked: {len(historical)} lifecycle(s) follow hypgraph 0.3's rule, not the current "
281
+ f"one ({shown}). A bundle exported by 0.3 shows them so, but the chain doesn't record which version "
282
+ "exported a bundle, so this can't be told from an edited view. Go by the current rule.")
283
+
284
+
285
+ def _unconfirmed_note(unconfirmed: list) -> str:
286
+ return (f"The views are not checked: {len(unconfirmed)} node(s) are shown superseded with no replacement in this "
287
+ f"bundle ({_listed(unconfirmed)}). A replacement in another investigation would be among the redacted "
288
+ "events, which keep only their hashes, so this can't be confirmed here: the rows make them active. Only "
289
+ "the full record, verified against the published head, settles it.")
290
+
291
+
267
292
  def _j(v: Any) -> str:
268
293
  """A value in a problem, written as the browser's verifier writes it (JSON.stringify)."""
269
294
  return json.dumps(v, ensure_ascii=False, separators=(",", ":"))
@@ -309,6 +334,10 @@ def _scope_problem(b: dict, tables: dict, redacted: int) -> str:
309
334
  return ""
310
335
 
311
336
 
337
+ # the node types a supersedes or corrects link can point at: no other type can be superseded
338
+ REPLACEABLE = frozenset().union(*(RELS[r][1] for r in SAME_TYPE))
339
+
340
+
312
341
  def _eid(v: Any) -> int | None:
313
342
  """A link id as the browser's verifier reads one (Number.isInteger): 7 and 7.0, not true or "7"."""
314
343
  if isinstance(v, bool) or not isinstance(v, (int, float)) or not float(v).is_integer():
@@ -316,12 +345,24 @@ def _eid(v: Any) -> int | None:
316
345
  return int(v)
317
346
 
318
347
 
319
- def _derived_problem(b: dict, tables: dict) -> str:
348
+ def _derived_problem(b: dict, tables: dict, historical: list | None = None, unconfirmed: list | None = None,
349
+ redacted: int = 0) -> str:
320
350
  """The derived views are presentation, but a viewer shows them beside a verified chain: every fact in them that
321
351
  the rows fix — which investigations there are, which nodes and live links each has, each node's type, code,
322
352
  author, time, version, title and lifecycle, each link's ends and fields — must be present and agree with the
323
353
  verified rows. A view that leaves a fact out fails: a viewer would fill the gap with a default (a withdrawn claim
324
- shown as active). The rest of standing can't be checked this way."""
354
+ shown as active). The rest of standing can't be checked this way.
355
+
356
+ A lifecycle is checked against the current rule only. One that matches only 0.3's rule (the end of a chain of
357
+ replacements shown active) is no problem — a 0.3 export shows it so — but it isn't checked either: the chain
358
+ doesn't record which version exported a bundle, so it can't be told from an edited view. Each such node goes on
359
+ ``historical`` as (name, shown, current), and the bundle's views are reported as historical, not checked.
360
+
361
+ A bundle with no redacted events holds every link, scoped or not, so a lifecycle the rows don't give fails. A
362
+ scoped bundle with redacted events leaves out links from other investigations, and one of them may replace a node:
363
+ a node of a type that can be replaced (``REPLACEABLE``) shown superseded with no replacement in the rows goes on
364
+ ``unconfirmed`` by name, and the views are reported as unconfirmed, not checked. ``scope`` isn't covered by the
365
+ chain, so naming one never loosens anything on its own."""
325
366
  d = b.get("derived")
326
367
  if d is None:
327
368
  return ""
@@ -373,13 +414,20 @@ def _derived_problem(b: dict, tables: dict) -> str:
373
414
  st = x.get("state")
374
415
  if not isinstance(st, dict) or "lifecycle" not in st:
375
416
  return f"{where} shows {name} without its lifecycle"
376
- w = lifecycle(n["id"], True)
377
- allowed = {w, lifecycle(n["id"], False)}
378
- if scope and n["id"] not in withdrawn:
379
- allowed.add("superseded") # a scoped bundle leaves out links to other investigations: one may replace it
380
- if st["lifecycle"] not in allowed:
381
- return f"{where} shows {name} as {_j(st['lifecycle'])}; the record makes it {_j(w)}"
382
- return ""
417
+ w, shown = lifecycle(n["id"], True), st["lifecycle"]
418
+ if shown == w:
419
+ return ""
420
+ if (shown == "superseded" and scope and redacted and n["id"] not in withdrawn
421
+ and n["type"] in REPLACEABLE):
422
+ # a replacement in another investigation is among the redacted events: possible, not confirmable
423
+ if unconfirmed is not None and name not in unconfirmed:
424
+ unconfirmed.append(name)
425
+ return ""
426
+ if shown == lifecycle(n["id"], False):
427
+ if historical is not None and (name, shown, w) not in historical:
428
+ historical.append((name, shown, w))
429
+ return ""
430
+ return f"{where} shows {name} as {_j(shown)}; the record makes it {_j(w)}"
383
431
  by_inv = d.get("by_inv")
384
432
  if not isinstance(by_inv, dict) or set(by_inv) != invs:
385
433
  return "the views don't cover the record's investigations"
@@ -297,6 +297,11 @@ def cmd_verify(args) -> int:
297
297
  text += "\n coverage: PARTIAL — not every row field is vouched for by its event (see below)"
298
298
  if ok and res.get("views") == "none":
299
299
  text += "\n no derived views: exported without them, so a viewer has nothing to show beyond the rows"
300
+ if ok and res.get("views") == "historical":
301
+ text += "\n views: NOT CHECKED — some lifecycles follow hypgraph 0.3's rule, not the current one (see the note)"
302
+ if ok and res.get("views") == "unconfirmed":
303
+ text += ("\n views: NOT CHECKED — some nodes are shown superseded by something outside this bundle, which "
304
+ "can't be confirmed here (see the note)")
300
305
  if ok and res.get("unchecked"):
301
306
  text += ("\n not covered (written before events hashed these fields): "
302
307
  + ", ".join(f"{f} ×{n}" for f, n in sorted(res["unchecked"].items())))
@@ -48,6 +48,7 @@
48
48
  .hg .hg-search { display: flex; align-items: center; gap: 8px; width: 260px; padding: 6px 10px; border: 1px solid var(--line);
49
49
  border-radius: 8px; background: var(--surface2); color: var(--ink3); cursor: pointer; }
50
50
  .hg .hg-search kbd { margin-left: auto; font: 11px var(--mono); padding: 1px 6px; border: 1px solid var(--line2); border-radius: 4px; }
51
+ .hg .hg-lang { padding: 5px 6px; font-size: 12.5px; max-width: 112px; cursor: pointer; } /* the language menu */
51
52
  .hg .btn { border: 1px solid var(--line); background: var(--surface); border-radius: 8px; padding: 6px 10px; cursor: pointer;
52
53
  display: inline-flex; gap: 6px; align-items: center; white-space: nowrap; }
53
54
  .hg .btn:hover { background: var(--sunken); } .hg .btn:disabled { opacity: .5; cursor: default; background: var(--surface); } .hg .btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
@@ -117,6 +118,7 @@
117
118
  .hg .note { display: flex; gap: 10px; align-items: flex-start; padding: 9px 14px; border-radius: 9px; margin-bottom: 14px;
118
119
  background: var(--accent-soft); color: var(--accent-ink); font-size: 12.5px; }
119
120
  .hg .note.warn { background: var(--warn-soft); color: var(--warn); }
121
+ .hg .note[data-exported] { display: block; } /* one sentence with inline code: keep it one paragraph */
120
122
 
121
123
  /* stages and gates */
122
124
  .hg .bar { display: flex; gap: 3px; }