hypgraph 0.7.7__tar.gz → 0.7.9__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 (103) hide show
  1. {hypgraph-0.7.7 → hypgraph-0.7.9}/AGENTS.md +2 -0
  2. {hypgraph-0.7.7 → hypgraph-0.7.9}/CHANGELOG.md +35 -0
  3. hypgraph-0.7.9/DISCLAIMER.md +36 -0
  4. {hypgraph-0.7.7 → hypgraph-0.7.9}/PKG-INFO +7 -1
  5. {hypgraph-0.7.7 → hypgraph-0.7.9}/README.md +6 -0
  6. {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/spec.md +8 -1
  7. {hypgraph-0.7.7 → hypgraph-0.7.9}/pyproject.toml +1 -1
  8. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/__init__.py +3 -2
  9. hypgraph-0.7.9/src/hypgraph/blind.py +414 -0
  10. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/coord.py +79 -2
  11. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/derive.py +15 -1
  12. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/graph.py +26 -0
  13. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/mcp.py +88 -40
  14. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/plan.py +2 -1
  15. hypgraph-0.7.9/src/hypgraph/references.py +173 -0
  16. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/schema.py +7 -3
  17. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/server.py +12 -1
  18. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/skill/SKILL.md +3 -0
  19. hypgraph-0.7.9/tests/test_blind.py +168 -0
  20. hypgraph-0.7.9/tests/test_references.py +156 -0
  21. {hypgraph-0.7.7 → hypgraph-0.7.9}/.gitignore +0 -0
  22. {hypgraph-0.7.7 → hypgraph-0.7.9}/CITATION.cff +0 -0
  23. {hypgraph-0.7.7 → hypgraph-0.7.9}/LICENSE +0 -0
  24. {hypgraph-0.7.7 → hypgraph-0.7.9}/NOTICE +0 -0
  25. {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/PLAN.md +0 -0
  26. {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
  27. {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/decisions/ADR-002-governance-profiles.md +0 -0
  28. {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/profile-workflows-plan.md +0 -0
  29. {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/review-2026-10-24.md +0 -0
  30. {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/site-plan.md +0 -0
  31. {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/ui-plan.md +0 -0
  32. {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/workflows/README.md +0 -0
  33. {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/workflows/knowledge.md +0 -0
  34. {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/workflows/optimization.md +0 -0
  35. {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/workflows/scientific.md +0 -0
  36. {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/workflows/software.md +0 -0
  37. {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/workflows/trading.md +0 -0
  38. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/__main__.py +0 -0
  39. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/bundle.py +0 -0
  40. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/checks.py +0 -0
  41. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/cli.py +0 -0
  42. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/demos/__init__.py +0 -0
  43. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/demos/broad_street.py +0 -0
  44. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/language.py +0 -0
  45. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/policy.py +0 -0
  46. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/profiles.py +0 -0
  47. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/refusals.py +0 -0
  48. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/report_check.py +0 -0
  49. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/stats.py +0 -0
  50. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/store.py +0 -0
  51. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/__init__.py +0 -0
  52. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/atlas.css +0 -0
  53. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/atlas.js +0 -0
  54. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/hypgraph.css +0 -0
  55. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/hypgraph.js +0 -0
  56. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/hypgraph.svg +0 -0
  57. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/index.html +0 -0
  58. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
  59. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/vendor/cose-base.js +0 -0
  60. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
  61. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
  62. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/vendor/layout-base.js +0 -0
  63. {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/workflow.py +0 -0
  64. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_accept_work_kind.py +0 -0
  65. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_bound_support.py +0 -0
  66. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_bundle_telemetry.py +0 -0
  67. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_cancel_releases_lease.py +0 -0
  68. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_cited_and_failed_support.py +0 -0
  69. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_coord.py +0 -0
  70. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_d10.py +0 -0
  71. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_engine.py +0 -0
  72. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_engine_fixes.py +0 -0
  73. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_incremental.py +0 -0
  74. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_link_translate.py +0 -0
  75. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_log_and_check.py +0 -0
  76. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_measure_questions.py +0 -0
  77. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_opposed_answers.py +0 -0
  78. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_option_lead.py +0 -0
  79. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_order_detail.py +0 -0
  80. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_profiles.py +0 -0
  81. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_profiles_041.py +0 -0
  82. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_questions_not_assertions.py +0 -0
  83. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_refusal_friction.py +0 -0
  84. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_refusal_results.py +0 -0
  85. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_refusals.py +0 -0
  86. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_relation_words.py +0 -0
  87. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_report_answers.py +0 -0
  88. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_rerunnable.py +0 -0
  89. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_restates_scale.py +0 -0
  90. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_server_auth.py +0 -0
  91. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_server_hosts.py +0 -0
  92. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_shape_refusals.py +0 -0
  93. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_stats_not_backtests.py +0 -0
  94. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_stats_values.py +0 -0
  95. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_surfaces.py +0 -0
  96. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_telemetry_stream.py +0 -0
  97. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_ui_app.py +0 -0
  98. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_ui_engine.py +0 -0
  99. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_uncorrected_effect.py +0 -0
  100. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_unnamed_inputs.py +0 -0
  101. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_versions_not_figures.py +0 -0
  102. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_vocabulary_friction.py +0 -0
  103. {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_workflows.py +0 -0
@@ -3,6 +3,8 @@
3
3
  You are an AI agent and you've been pointed at HypGraph. This page is for you; https://hypgraph.com/llms.txt is the
4
4
  same in short.
5
5
 
6
+ **What standing is not.** Standing is derived from what was written, by the record's rules: never present it as a certification, a peer review or advice (https://hypgraph.com/disclaimer/).
7
+
6
8
  **What it is.** A research record you write into and can't flatter. You add questions, hypotheses, experiments, runs,
7
9
  results, claims, sources, reviews and decisions. You can't set how well anything is known: review state, risk,
8
10
  independent support and evidence strength are worked out from the record each time it's read. Every write is a
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.9 — 2026-10-30: blind work orders
4
+
5
+ - **A checker can measure without seeing the answer it checks.** `issue_work(..., blind=True)` (MCP `issue_work`
6
+ `blind=true`, HTTP write `blind`) issues a work order whose assignee works blind until it returns the order.
7
+ The order text gives the objective, the method and the target by code only. Hypotheses and their predictions,
8
+ results, claims, reviews, runs and their code, sources, other orders and other teams' attempts are sealed.
9
+ `hypgraph.blind` holds the rules:
10
+ - **At issue**, a blind order is refused with a prediction, with a predicate that has to read its target
11
+ (`reviewed`, `no_open_objection`, `report_clean`), for an assignee that has already written in the record, and
12
+ when its objective, method or note quote a number only the sealed material carries.
13
+ - **At read**, the MCP server seals its seat while that seat holds an open blind order. The tools that summarise
14
+ what others wrote are refused, `show` refuses a sealed node, `find` leaves sealed nodes out, and every number
15
+ only the sealed material carries is scrubbed from what comes back. `blind.dispatch(g, ..., keep=)` gives a host
16
+ the same redaction for its HTTP routes, per reader (HypMesh uses it). `hypgraph serve` and the Python API read
17
+ the whole record: a file is only as blind as whoever holds it.
18
+ - **In standing**, a result its assignee wrote inside the window is `blind` (and `sealed` while the window is
19
+ open). After the return, a blind result that repeats an earlier result's numbers `agrees_blind` with it
20
+ instead of restating it, and a claim or hypothesis counts its `blind_lines`: "2 independent lines of support
21
+ (1 written blind)". A result revised after the window closed is no longer blind.
22
+ - The record sees writes, not reads. A seat that read the record before its order was issued has already seen
23
+ what is now sealed: give a checker its order first.
24
+
25
+ ## 0.7.8 — 2026-10-29: pinned references between records
26
+
27
+ - **A record can cite a node of another record and prove what it read.** A `source` takes four more fields,
28
+ `record` (the cited record's URL), `node`, `version` and `hash` (sha256 of that version's canonical body, the
29
+ same hash the cited record's chain holds), all four or none (`SHAPE_REFERENCE` otherwise). `Graph.pin(node,
30
+ record_url)` / `GET /api/graph/pin/{id}?record=` build them for one of this record's nodes, with the node's own
31
+ words in `citation` and `says`, so the citing record reads without the cited one. `Graph.references(inv)` /
32
+ `GET /api/graph/references` list the sources here that pin other records, with who cites them.
33
+ `hypgraph.verify_reference(fields, fetch=)` reads the cited record's node route (HTTP, or a host's own reader)
34
+ and reports whether the pinned version still hashes to the pin, whether it was revised since, and whether it
35
+ was withdrawn. The citing record never trusts the other's status; it trusts the hash it pinned. A record stays
36
+ one trust boundary; a host that holds many (a mesh) derives the network from these pins.
37
+
3
38
  ## 0.7.7 — 2026-10-29: `hypgraph serve` inside a container
4
39
 
5
40
  - **`hypgraph serve` bound to `0.0.0.0` answered 421 to everyone.** Its Host check knew only the bind address,
@@ -0,0 +1,36 @@
1
+ # Disclaimer
2
+
3
+ ## Provided as is
4
+
5
+ HypGraph is provided **as is, without warranty of any kind**, under the
6
+ [Apache License 2.0](https://github.com/voodoomods/hypgraph/blob/main/LICENSE). Sections 7 and 8 of that licence
7
+ (Disclaimer of Warranty, Limitation of Liability) apply to the code, the record format, the documentation, the
8
+ examples and the website.
9
+
10
+ ## Standing is derived, not certified
11
+
12
+ A record's standing (checked, verified, replicated, contested, at risk,
13
+ stale) is worked out from what was written into it, by rules you can read in the
14
+ [spec](https://hypgraph.com/spec/) and the code. It is not a certification, a peer review, an audit, or an
15
+ endorsement by VoodooMods LLC. The rules catch some ways a record can overclaim; they cannot catch a measurement that
16
+ was wrong when it was taken, a source that misreports, or a review done carelessly.
17
+
18
+ ## A verified chain proves integrity, not truth
19
+
20
+ `hypgraph verify` shows that a record or bundle has not been
21
+ changed since it was written. It does not show that what was written is correct.
22
+
23
+ ## Not advice
24
+
25
+ Nothing in a record, report, profile or example is legal, medical, financial or investment advice.
26
+ The trading profile and its examples work on paper positions and historical data; past results do not predict
27
+ future ones.
28
+
29
+ ## Your records are yours to answer for
30
+
31
+ You decide what goes into a record and what you do with what comes out,
32
+ including when you share a bundle or a report. Records are append-only: a withdrawal is written, not deleted, so do
33
+ not put anything into a record that you would need to erase.
34
+
35
+ HypGraph is made by VoodooMods LLC. Questions: open an issue at
36
+ https://github.com/voodoomods/hypgraph/issues.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hypgraph
3
- Version: 0.7.7
3
+ Version: 0.7.9
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/
@@ -245,6 +245,12 @@ affect, and a randomized suite checks that against a full rebuild. Run `python d
245
245
 
246
246
  HypGraph is made by **Nicolas Houghtaling** at **[VoodooMods LLC](https://github.com/voodoomods)**.
247
247
 
248
+ ## Disclaimer
249
+
250
+ Provided as is, without warranty. Standing is derived from what was written into a record, by rules you can
251
+ read; it is not a certification or an endorsement, a verified chain proves a record unchanged rather than correct, and
252
+ nothing here is legal, medical or financial advice. The full text: [DISCLAIMER.md](https://github.com/voodoomods/hypgraph/blob/main/DISCLAIMER.md).
253
+
248
254
  ## License
249
255
 
250
256
  Code: [Apache-2.0](https://github.com/voodoomods/hypgraph/blob/main/LICENSE). The record format ([docs/spec.md](https://github.com/voodoomods/hypgraph/blob/main/docs/spec.md)) and the docs: CC BY 4.0. Cite it with
@@ -213,6 +213,12 @@ affect, and a randomized suite checks that against a full rebuild. Run `python d
213
213
 
214
214
  HypGraph is made by **Nicolas Houghtaling** at **[VoodooMods LLC](https://github.com/voodoomods)**.
215
215
 
216
+ ## Disclaimer
217
+
218
+ Provided as is, without warranty. Standing is derived from what was written into a record, by rules you can
219
+ read; it is not a certification or an endorsement, a verified chain proves a record unchanged rather than correct, and
220
+ nothing here is legal, medical or financial advice. The full text: [DISCLAIMER.md](https://github.com/voodoomods/hypgraph/blob/main/DISCLAIMER.md).
221
+
216
222
  ## License
217
223
 
218
224
  Code: [Apache-2.0](https://github.com/voodoomods/hypgraph/blob/main/LICENSE). The record format ([docs/spec.md](https://github.com/voodoomods/hypgraph/blob/main/docs/spec.md)) and the docs: CC BY 4.0. Cite it with
@@ -28,7 +28,7 @@ correction is a new row, never an edited one.
28
28
  | `run` | RUN | **kind**\*, job_id, name, state, code, inputs, outputs, args, config, seed, env, started, finished, exit_code, log_sha256, note | One execution: code, inputs and outputs hashed, seed, state. `kind=lab` only from an instrument. |
29
29
  | `result` | RS | **summary**\*, kind, outcome, values, expected, limitations, power, evidence_note, legacy | What a run (or an observation, or a citation) found: values, outcome, power. |
30
30
  | `artifact` | A | **sha256**\*, path, size, format | Exact bytes, by sha256 — data, code, a model, a figure. |
31
- | `source` | S | **citation**\*, doi, url, passage, retrieved, says | Outside literature or data: citation, DOI, the passage that says it. |
31
+ | `source` | S | **citation**\*, doi, url, passage, retrieved, says, kind, reliability, record, node, version, hash | Outside literature or data: citation, DOI, the passage that says it. Or another record's node, pinned: `record` (its URL), `node`, `version` and `hash` (sha256 of that version's canonical body) — all four or none. |
32
32
  | `review` | RV | **verdict**\*, **method**\*, **notes**\*, official | A verdict on one version of a claim, result, run, experiment or hypothesis. |
33
33
  | `decision` | D | **choice**\*, alternatives, **reasons**\*, revisit_if, meeting | A choice made on the evidence; flagged when what it was based on changes. |
34
34
  | `task` | T | **kanban_id**\*, title | A unit of work in your tracker, linked to what it works on. |
@@ -144,6 +144,13 @@ Implementations must derive these, never store them:
144
144
  - A replication edge on the run raises the profile according to its `differs`.
145
145
  - A result of `kind=cited` is external only if it `cites` a source node.
146
146
  - **basis_changed** (decisions): a `based_on` edge whose target's version has moved past the pinned `dst_v`.
147
+ - **written blind** (results): a work order with `blind: true` opens a window at its creation event and closes it at
148
+ the first release (return or cancel) or withdrawal of any order in its rework chain. A result its assignee
149
+ created inside the window, and has not revised since it closed, is written blind under that order, provided the
150
+ assignee had created nothing in the record before the window opened. While the window is open the result is
151
+ `sealed`. Once it is closed, earlier results whose numbers it repeats are `agrees_blind`, not `restates`.
152
+ - **blind lines** (claims, hypotheses): independent lines whose group contains a result written blind under a
153
+ closed window.
147
154
 
148
155
  ## 6. Write rules (normative for writers)
149
156
 
@@ -46,7 +46,7 @@ path = "src/hypgraph/__init__.py"
46
46
  packages = ["src/hypgraph"]
47
47
 
48
48
  [tool.hatch.build.targets.sdist]
49
- include = ["src/hypgraph", "tests", "docs", "README.md", "LICENSE", "NOTICE", "CITATION.cff", "AGENTS.md", "CHANGELOG.md"]
49
+ include = ["src/hypgraph", "tests", "docs", "README.md", "DISCLAIMER.md", "LICENSE", "NOTICE", "CITATION.cff", "AGENTS.md", "CHANGELOG.md"]
50
50
 
51
51
  [tool.pytest.ini_options]
52
52
  testpaths = ["tests", "src/hypgraph/__init__.py"]
@@ -16,13 +16,14 @@ 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.7"
19
+ __version__ = "0.7.9"
20
20
 
21
21
  from .bundle import verify_bundle # noqa: E402
22
22
  from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
23
23
  from .policy import Policy # noqa: E402
24
+ from .references import verify_reference # noqa: E402
24
25
  from .schema import FORMAT, describe # noqa: E402
25
26
  from .store import verify_file # noqa: E402
26
27
 
27
- __all__ = ["Graph", "GraphError", "Policy", "Author", "verify_bundle", "verify_file", "file_sha256", "describe",
28
+ __all__ = ["Graph", "GraphError", "Policy", "Author", "verify_bundle", "verify_file", "file_sha256", "describe", "verify_reference",
28
29
  "FORMAT", "__version__"]
@@ -0,0 +1,414 @@
1
+ """
2
+ Blind work orders: a checker measures without seeing the answer it is checking.
3
+
4
+ A work order issued with ``blind=True`` seals the record from its assignee until the order is returned. While it is
5
+ open, the assignee reads the problem statement (the investigation the order belongs to, the curators' investigations,
6
+ the questions) and its own order; everything else other authors wrote — hypotheses and their predictions, results,
7
+ claims, reviews, runs and their code, sources, other orders, other teams' attempts — is sealed: shown as a placeholder,
8
+ left out of search, its numbers scrubbed wherever else they appear.
9
+
10
+ What the assignee writes inside that window is **written blind**. When the window closes (the order is returned, or
11
+ cancelled), the record compares: a blind result that repeats an earlier result's numbers *agrees blind* with it
12
+ instead of restating it, and a line of support that contains a blind result is counted as a blind line.
13
+
14
+ The record enforces what it can see:
15
+
16
+ - at issue: a blind order carries no prediction, asks for a measurement (not a review or a report), goes to an
17
+ assignee that has written nothing in this record yet, and its objective, method and note quote no number that
18
+ only the sealed material carries;
19
+ - at read: :func:`view_for` gives the redaction a host applies to everything it returns to a reader while an order
20
+ is open — the MCP server (``hypgraph mcp``) for its seat's author, HypMesh for every reader but the agent that
21
+ wrote a node. ``hypgraph serve`` and the Python API read the whole record: a file is only as blind as whoever
22
+ holds it.
23
+
24
+ It cannot see reads. A seat that read the record before its order was issued has already seen what is now sealed;
25
+ give a checker its order first.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import re
31
+ from dataclasses import dataclass, field
32
+ from typing import Any, Callable
33
+
34
+ from . import derive as D
35
+
36
+ # Numbers as written: 10,000 · 1229 · 0.0371 · -2.5. Not inside words or ids (n-1a2b3c, X12, v2).
37
+ _NUM = re.compile(r"(?<![\w.,])-?(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?(?![\w])")
38
+ # A reader may make these requests while sealed; everything else quotes or summarises what other authors wrote.
39
+ READ_ROUTES = frozenset({"investigations", "orders", "order", "resolve", "subgraph", "node", "why", "impact",
40
+ "neighborhood", "search", "events", "verify", "profiles", "profile", "references"})
41
+ POST_ROUTES = frozenset({"check_work"})
42
+ VISIBLE_TYPES = frozenset({"question"})
43
+ # A blind order asks for a measurement. A review or a report has to read what it judges.
44
+ BLIND_PREDICATES = frozenset({"run_registered", "result_exists", "claim_made", "within_budget"})
45
+ SEALED = "sealed"
46
+
47
+
48
+ def canon(tok: str) -> str:
49
+ """A number token as compared: no thousands separators, no trailing zeros after the point."""
50
+ t = tok.replace(",", "")
51
+ if "." in t:
52
+ t = t.rstrip("0").rstrip(".")
53
+ return t or "0"
54
+
55
+
56
+ def _significant(t: str) -> bool:
57
+ """Small whole numbers (0–12) are everywhere — counts, steps, versions — and say nothing on their own."""
58
+ try:
59
+ f = float(t)
60
+ except ValueError:
61
+ return False
62
+ return not (f.is_integer() and abs(f) <= 12)
63
+
64
+
65
+ def numbers(x: Any) -> set[str]:
66
+ """Every significant number in a body, from its text and its values."""
67
+ out: set[str] = set()
68
+
69
+ def walk(v: Any) -> None:
70
+ if isinstance(v, bool) or v is None:
71
+ return
72
+ if isinstance(v, (int, float)):
73
+ t = canon(repr(float(v))) if isinstance(v, float) else str(v)
74
+ if _significant(t):
75
+ out.add(t)
76
+ elif isinstance(v, str):
77
+ out.update(c for c in (canon(m.group(0)) for m in _NUM.finditer(v)) if _significant(c))
78
+ elif isinstance(v, dict):
79
+ for w in v.values():
80
+ walk(w)
81
+ elif isinstance(v, (list, tuple)):
82
+ for w in v:
83
+ walk(w)
84
+ walk(x)
85
+ return out
86
+
87
+
88
+ # ── windows: when an order's assignee works blind ──────────────────
89
+ def _is_order(s: D.Snap, i: str) -> bool:
90
+ n = s.nodes.get(i)
91
+ return bool(n and n["type"] == "task" and n["body"].get("assignee") and n["body"].get("objective"))
92
+
93
+
94
+ def _create_seqs(s: D.Snap) -> dict[str, int]:
95
+ return {e["ref"]: e["seq"] for e in s.events if e["kind"] == "create"}
96
+
97
+
98
+ @dataclass
99
+ class Window:
100
+ root: str # the first order of the chain (a rework keeps its window)
101
+ orders: list[str] # every order of the chain, oldest first
102
+ assignee: str
103
+ inv: str
104
+ start: int # seq the root was created at
105
+ end: int | None # seq of the first release (return or cancel) or withdrawal in the chain
106
+ sealing: bool # the window is open and the chain's current order is open or overdue
107
+ fresh: bool # the assignee had written nothing in this record before the window opened
108
+
109
+ def code(self, s: D.Snap) -> str:
110
+ return s.nodes[self.orders[-1]]["code"]
111
+
112
+
113
+ def windows(s: D.Snap) -> list[Window]:
114
+ """Every blind order chain in the record, with its window. Cached on the snapshot by its head."""
115
+ hit = getattr(s, "_blind_windows", None)
116
+ if hit is not None and hit[0] == s.seq:
117
+ return hit[1]
118
+ from . import coord
119
+ roots: dict[str, list[str]] = {}
120
+ for i, n in s.nodes.items():
121
+ if _is_order(s, i) and n["body"].get("blind"):
122
+ roots.setdefault(coord.chain_root(s, i), []).append(i)
123
+ out: list[Window] = []
124
+ if roots:
125
+ created = _create_seqs(s)
126
+ first_write: dict[str, int] = {}
127
+ for e in s.events:
128
+ if e["kind"] == "create" and e["actor"] not in first_write:
129
+ first_write[e["actor"]] = e["seq"]
130
+ for root, chain in roots.items():
131
+ if not s.nodes[root]["body"].get("blind"):
132
+ continue # a rework made blind after a sighted attempt isn't blind
133
+ chain = sorted(set(chain) | {root}, key=lambda o: created.get(o, 0))
134
+ members = set(chain)
135
+ start = created.get(root, 0)
136
+ end = None
137
+ for e in s.events:
138
+ if e["seq"] <= start:
139
+ continue
140
+ if (e["kind"] == "lease_release" and (e["payload"] or {}).get("task") in members) or \
141
+ (e["kind"] == "withdraw" and e["ref"] in members):
142
+ end = e["seq"]
143
+ break
144
+ who = s.nodes[root]["body"]["assignee"]
145
+ cur = chain[-1]
146
+ status = coord.order_status(s, cur)
147
+ out.append(Window(root, chain, who, s.nodes[root]["inv"], start, end,
148
+ sealing=end is None and status in ("open", "overdue"),
149
+ fresh=first_write.get(who, start + 1) > start))
150
+ s._blind_windows = (s.seq, out)
151
+ return out
152
+
153
+
154
+ def written_blind(s: D.Snap) -> dict[str, Window]:
155
+ """Results the assignee of a blind order wrote inside its window, unrevised since it closed. An assignee that
156
+ had written in the record before the order is not fresh: nothing it writes counts as blind."""
157
+ hit = getattr(s, "_blind_results", None)
158
+ if hit is not None and hit[0] == s.seq:
159
+ return hit[1]
160
+ out: dict[str, Window] = {}
161
+ ws = [w for w in windows(s) if w.fresh]
162
+ if ws:
163
+ created = _create_seqs(s)
164
+ last_rev: dict[str, int] = {}
165
+ for e in s.events:
166
+ if e["kind"] == "revise":
167
+ last_rev[e["ref"]] = e["seq"]
168
+ for i, n in s.nodes.items():
169
+ if n["type"] != "result":
170
+ continue
171
+ c = created.get(i, 0)
172
+ for w in ws:
173
+ if n["created_by"] == w.assignee and c > w.start and (w.end is None or c < w.end) and \
174
+ (w.end is None or last_rev.get(i, 0) < w.end):
175
+ out[i] = w
176
+ break
177
+ s._blind_results = (s.seq, out)
178
+ return out
179
+
180
+
181
+ def blind_state(s: D.Snap, nid: str) -> dict:
182
+ """What derive.state adds for blindness: on a result, the order it was written blind under, and once the window
183
+ closed, the earlier results it agrees with (instead of restating them); on a claim or hypothesis, how many of
184
+ its independent lines contain a blind result whose window has closed."""
185
+ n = s.nodes[nid]
186
+ out: dict = {}
187
+ wb = written_blind(s)
188
+ if n["type"] == "result" and nid in wb:
189
+ w = wb[nid]
190
+ out["blind"] = w.code(s)
191
+ if w.end is None:
192
+ out["sealed"] = True
193
+ if n["type"] in ("claim", "hypothesis") and wb:
194
+ closed = {r for r, w in wb.items() if w.end is not None}
195
+ if closed:
196
+ k = _blind_lines(s, nid, closed)
197
+ if k:
198
+ out["blind_lines"] = k
199
+ return out
200
+
201
+
202
+ def _blind_lines(s: D.Snap, nid: str, blind: set[str]) -> int:
203
+ lines = [e["src"] for e in D.supporters(s, nid) if not D.fallen(s, e["src"])]
204
+ groups: list[tuple[set, bool]] = []
205
+ for x in lines:
206
+ r = D.roots(s, x)
207
+ is_blind = x in blind or bool(r & {D.producing_run(s, b) for b in blind if D.producing_run(s, b)}) \
208
+ or any(b in blind for b in _derived(s, x))
209
+ merged = [g for g in groups if g[0] & r]
210
+ for g in merged:
211
+ groups.remove(g)
212
+ r = r | g[0]
213
+ is_blind = is_blind or g[1]
214
+ groups.append((r, is_blind))
215
+ return sum(1 for _r, b in groups if b)
216
+
217
+
218
+ def _derived(s: D.Snap, x: str, limit: int = 60) -> set[str]:
219
+ out, stack = set(), [x]
220
+ while stack and len(out) < limit:
221
+ y = stack.pop()
222
+ for e in s.out.get(y, []):
223
+ if e["rel"] == "derived_from" and e["dst"] not in out:
224
+ out.add(e["dst"])
225
+ stack.append(e["dst"])
226
+ return out
227
+
228
+
229
+ # ── what is sealed, and from whom ──────────────────────────────────
230
+ def public(s: D.Snap, n: dict, w: Window) -> bool:
231
+ """The problem statement every reader keeps: questions, the curators' investigations, the order's own
232
+ investigation, and the order chain itself."""
233
+ return (n["type"] in VISIBLE_TYPES
234
+ or (n["type"] == "investigation" and (n["created_by"] in s.policy.curators or n["id"] == w.inv)))
235
+
236
+
237
+ def scope(s: D.Snap, w: Window) -> set[str]:
238
+ """What a sealing window hides from its assignee: everything other authors wrote that isn't the problem
239
+ statement or the order chain."""
240
+ chain = set(w.orders)
241
+ return {i for i, n in s.nodes.items()
242
+ if i not in chain and n["created_by"] != w.assignee and not public(s, n | {"id": i}, w)}
243
+
244
+
245
+ @dataclass
246
+ class View:
247
+ """The redaction for one reader. Empty (``bool(view) is False``) when nothing is sealed from it."""
248
+ hide: dict[str, str] = field(default_factory=dict) # node id → the order code that seals it
249
+ secrets: frozenset = frozenset() # numbers only the sealed material carries
250
+
251
+ def __bool__(self) -> bool:
252
+ return bool(self.hide)
253
+
254
+ def because(self) -> str:
255
+ codes = sorted(set(self.hide.values()))
256
+ return ", ".join(codes)
257
+
258
+ def message(self, what: str = "this") -> str:
259
+ return (f"{what} is sealed while blind work order {self.because()} is open: the record is hidden from its "
260
+ "checker until the order is returned. Work from the order (my_work) and return it.")
261
+
262
+ def allows(self, method: str, head: str) -> bool:
263
+ return head in (POST_ROUTES if method == "POST" else READ_ROUTES)
264
+
265
+ def scrub(self, x: Any) -> Any:
266
+ """Every number only the sealed material carries, replaced in every string."""
267
+ if not self.secrets:
268
+ return x
269
+ if isinstance(x, str):
270
+ return _NUM.sub(lambda m: "[sealed]" if canon(m.group(0)) in self.secrets else m.group(0), x)
271
+ if isinstance(x, dict):
272
+ return {k: self.scrub(v) for k, v in x.items()}
273
+ if isinstance(x, list):
274
+ return [self.scrub(v) for v in x]
275
+ return x
276
+
277
+ def node(self, d: dict) -> dict:
278
+ """A sealed node as a reader sees it: what it is and where, not what it says."""
279
+ code = self.hide.get(d.get("id"), "")
280
+ st = d.get("state") or {}
281
+ out = {k: d[k] for k in ("id", "type", "code", "inv", "v", "by", "at") if k in d}
282
+ out |= {"title": f"sealed until {code} is returned", "kind": "", SEALED: code,
283
+ "state": {"lifecycle": st.get("lifecycle", "active"), SEALED: code,
284
+ "sentence": f"sealed until blind work order {code} is returned"}}
285
+ if "body" in d:
286
+ out["body"] = {SEALED: f"until {code} is returned"}
287
+ for k in ("out", "in"):
288
+ if k in d:
289
+ out[k] = [self.edge(e) for e in d[k]]
290
+ return out
291
+
292
+ def edge(self, e: dict) -> dict:
293
+ if e.get("src") in self.hide or e.get("dst") in self.hide:
294
+ return e | {"why": "", "data": {}}
295
+ return e
296
+
297
+ def event(self, e: dict) -> dict:
298
+ if e.get("ref") in self.hide:
299
+ p = e.get("payload") or {}
300
+ return e | {"payload": {k: p[k] for k in ("type", "code", "inv", "v", "task") if k in p}}
301
+ return e
302
+
303
+ def apply(self, x: Any) -> Any:
304
+ """Redact a response: sealed nodes become placeholders, edges touching them lose their reasons, events on
305
+ them lose their payload, and secret numbers are scrubbed everywhere."""
306
+ if not self:
307
+ return x
308
+ return self.scrub(self._walk(x))
309
+
310
+ def _walk(self, x: Any) -> Any:
311
+ if isinstance(x, list):
312
+ return [self._walk(v) for v in x]
313
+ if not isinstance(x, dict):
314
+ return x
315
+ if x.get("id") in self.hide and ("type" in x or "code" in x or "body" in x):
316
+ return self.node(x)
317
+ if "src" in x and "dst" in x and "rel" in x:
318
+ return self.edge(x)
319
+ if "seq" in x and "kind" in x and "ref" in x:
320
+ return self.event(x)
321
+ return {k: self._walk(v) for k, v in x.items()}
322
+
323
+ def search(self, rows: list[dict]) -> list[dict]:
324
+ """Search results without sealed nodes: a hit on a sealed body says what the body contains."""
325
+ return [r for r in rows if r.get("id") not in self.hide]
326
+
327
+
328
+ def view_for(s: D.Snap, keep: Callable[[str, dict], bool]) -> View:
329
+ """The redaction for a reader, while any blind order is sealing: every node in a sealing window's scope, except
330
+ those ``keep(node_id, node)`` says this reader may see (its own). Secret numbers are the ones in hidden bodies
331
+ that no visible node carries."""
332
+ hide: dict[str, str] = {}
333
+ for w in windows(s):
334
+ if not w.sealing:
335
+ continue
336
+ code = w.code(s)
337
+ for i in scope(s, w):
338
+ if i not in hide and not keep(i, s.nodes[i]):
339
+ hide[i] = code
340
+ if not hide:
341
+ return View()
342
+ hidden_nums: set[str] = set()
343
+ seen_nums: set[str] = set()
344
+ for i, n in s.nodes.items():
345
+ (hidden_nums if i in hide else seen_nums).update(numbers(n["body"]))
346
+ return View(hide, frozenset(hidden_nums - seen_nums))
347
+
348
+
349
+ def view_for_author(s: D.Snap, author: str) -> View:
350
+ """The redaction for an MCP seat writing as ``author``: sealed only while it holds an open blind order, and then
351
+ everything in that order's scope is hidden from it."""
352
+ mine = [w for w in windows(s) if w.sealing and w.assignee == author]
353
+ if not mine:
354
+ return View()
355
+ hide: dict[str, str] = {}
356
+ for w in mine:
357
+ for i in scope(s, w):
358
+ hide.setdefault(i, w.code(s))
359
+ hidden_nums: set[str] = set()
360
+ seen_nums: set[str] = set()
361
+ for i, n in s.nodes.items():
362
+ (hidden_nums if i in hide else seen_nums).update(numbers(n["body"]))
363
+ return View(hide, frozenset(hidden_nums - seen_nums))
364
+
365
+
366
+ def dispatch(g, method: str, path: str, *, query: dict | None = None, body: dict | None = None,
367
+ keep: Callable[[str, dict], bool] = lambda i, n: False, **kw) -> tuple[int, Any]:
368
+ """``server.dispatch`` for a reader while blind orders may be open: refused where a route would quote sealed
369
+ material, redacted everywhere else. ``keep`` says which nodes this reader may see anyway (its own)."""
370
+ from .server import dispatch as plain
371
+ s = g.snapshot()
372
+ if not windows(s):
373
+ return plain(g, method, path, query=query, body=body, **kw)
374
+ v = view_for(s, keep)
375
+ if not v:
376
+ return plain(g, method, path, query=query, body=body, **kw)
377
+ parts = [p for p in path.strip("/").split("/") if p]
378
+ head, arg = (parts[0] if parts else ""), (parts[1] if len(parts) > 1 else "")
379
+ if method == "POST" and head == "write":
380
+ status, out = plain(g, method, path, query=query, body=body, **kw)
381
+ return status, v.apply(out)
382
+ if not v.allows(method, head):
383
+ return 409, {"detail": v.message(f"/{head}"), SEALED: v.because()}
384
+ if head in ("order", "node", "why", "impact", "neighborhood") and arg:
385
+ try:
386
+ nid = g.resolve(arg)
387
+ except Exception:
388
+ nid = arg
389
+ if head == "order" and nid in v.hide:
390
+ return 409, {"detail": v.message(f"order {arg}"), SEALED: v.because()}
391
+ status, out = plain(g, method, path, query=query, body=body, **kw)
392
+ if head == "search" and status == 200 and isinstance(out, dict) and isinstance(out.get("results"), list):
393
+ out = out | {"results": v.search(out["results"])}
394
+ return status, v.apply(out)
395
+
396
+
397
+ def leaks(s: D.Snap, w: Window, texts: list[str]) -> dict[str, list[str]]:
398
+ """Numbers an order's own text quotes that only the material it would seal carries: number → codes holding it."""
399
+ hide = scope(s, w)
400
+ visible: set[str] = set()
401
+ for i, n in s.nodes.items():
402
+ if i not in hide:
403
+ visible |= numbers(n["body"])
404
+ quoted = numbers(texts) - visible
405
+ out: dict[str, list[str]] = {}
406
+ for i in hide:
407
+ hit = quoted & numbers(s.nodes[i]["body"])
408
+ for x in hit:
409
+ out.setdefault(x, []).append(s.nodes[i]["code"] or i)
410
+ return {k: sorted(v) for k, v in out.items()}
411
+
412
+
413
+ __all__ = ["Window", "View", "windows", "written_blind", "blind_state", "scope", "view_for", "view_for_author",
414
+ "dispatch", "leaks", "numbers", "BLIND_PREDICATES"]