hypgraph 0.7.8__tar.gz → 0.7.10__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.8 → hypgraph-0.7.10}/AGENTS.md +2 -0
  2. {hypgraph-0.7.8 → hypgraph-0.7.10}/CHANGELOG.md +42 -0
  3. hypgraph-0.7.10/DISCLAIMER.md +36 -0
  4. {hypgraph-0.7.8 → hypgraph-0.7.10}/PKG-INFO +7 -1
  5. {hypgraph-0.7.8 → hypgraph-0.7.10}/README.md +6 -0
  6. {hypgraph-0.7.8 → hypgraph-0.7.10}/docs/spec.md +7 -0
  7. {hypgraph-0.7.8 → hypgraph-0.7.10}/pyproject.toml +1 -1
  8. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/__init__.py +1 -1
  9. hypgraph-0.7.10/src/hypgraph/blind.py +415 -0
  10. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/coord.py +79 -2
  11. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/derive.py +15 -1
  12. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/graph.py +11 -0
  13. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/mcp.py +93 -43
  14. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/plan.py +2 -1
  15. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/schema.py +7 -1
  16. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/server.py +1 -1
  17. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/skill/SKILL.md +3 -0
  18. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/ui/hypgraph.css +2 -1
  19. hypgraph-0.7.10/tests/test_blind.py +214 -0
  20. {hypgraph-0.7.8 → hypgraph-0.7.10}/.gitignore +0 -0
  21. {hypgraph-0.7.8 → hypgraph-0.7.10}/CITATION.cff +0 -0
  22. {hypgraph-0.7.8 → hypgraph-0.7.10}/LICENSE +0 -0
  23. {hypgraph-0.7.8 → hypgraph-0.7.10}/NOTICE +0 -0
  24. {hypgraph-0.7.8 → hypgraph-0.7.10}/docs/PLAN.md +0 -0
  25. {hypgraph-0.7.8 → hypgraph-0.7.10}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
  26. {hypgraph-0.7.8 → hypgraph-0.7.10}/docs/decisions/ADR-002-governance-profiles.md +0 -0
  27. {hypgraph-0.7.8 → hypgraph-0.7.10}/docs/profile-workflows-plan.md +0 -0
  28. {hypgraph-0.7.8 → hypgraph-0.7.10}/docs/review-2026-10-24.md +0 -0
  29. {hypgraph-0.7.8 → hypgraph-0.7.10}/docs/site-plan.md +0 -0
  30. {hypgraph-0.7.8 → hypgraph-0.7.10}/docs/ui-plan.md +0 -0
  31. {hypgraph-0.7.8 → hypgraph-0.7.10}/docs/workflows/README.md +0 -0
  32. {hypgraph-0.7.8 → hypgraph-0.7.10}/docs/workflows/knowledge.md +0 -0
  33. {hypgraph-0.7.8 → hypgraph-0.7.10}/docs/workflows/optimization.md +0 -0
  34. {hypgraph-0.7.8 → hypgraph-0.7.10}/docs/workflows/scientific.md +0 -0
  35. {hypgraph-0.7.8 → hypgraph-0.7.10}/docs/workflows/software.md +0 -0
  36. {hypgraph-0.7.8 → hypgraph-0.7.10}/docs/workflows/trading.md +0 -0
  37. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/__main__.py +0 -0
  38. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/bundle.py +0 -0
  39. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/checks.py +0 -0
  40. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/cli.py +0 -0
  41. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/demos/__init__.py +0 -0
  42. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/demos/broad_street.py +0 -0
  43. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/language.py +0 -0
  44. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/policy.py +0 -0
  45. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/profiles.py +0 -0
  46. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/references.py +0 -0
  47. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/refusals.py +0 -0
  48. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/report_check.py +0 -0
  49. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/stats.py +0 -0
  50. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/store.py +0 -0
  51. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/ui/__init__.py +0 -0
  52. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/ui/atlas.css +0 -0
  53. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/ui/atlas.js +0 -0
  54. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/ui/hypgraph.js +0 -0
  55. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/ui/hypgraph.svg +0 -0
  56. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/ui/index.html +0 -0
  57. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
  58. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/ui/vendor/cose-base.js +0 -0
  59. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
  60. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
  61. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/ui/vendor/layout-base.js +0 -0
  62. {hypgraph-0.7.8 → hypgraph-0.7.10}/src/hypgraph/workflow.py +0 -0
  63. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_accept_work_kind.py +0 -0
  64. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_bound_support.py +0 -0
  65. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_bundle_telemetry.py +0 -0
  66. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_cancel_releases_lease.py +0 -0
  67. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_cited_and_failed_support.py +0 -0
  68. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_coord.py +0 -0
  69. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_d10.py +0 -0
  70. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_engine.py +0 -0
  71. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_engine_fixes.py +0 -0
  72. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_incremental.py +0 -0
  73. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_link_translate.py +0 -0
  74. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_log_and_check.py +0 -0
  75. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_measure_questions.py +0 -0
  76. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_opposed_answers.py +0 -0
  77. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_option_lead.py +0 -0
  78. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_order_detail.py +0 -0
  79. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_profiles.py +0 -0
  80. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_profiles_041.py +0 -0
  81. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_questions_not_assertions.py +0 -0
  82. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_references.py +0 -0
  83. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_refusal_friction.py +0 -0
  84. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_refusal_results.py +0 -0
  85. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_refusals.py +0 -0
  86. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_relation_words.py +0 -0
  87. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_report_answers.py +0 -0
  88. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_rerunnable.py +0 -0
  89. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_restates_scale.py +0 -0
  90. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_server_auth.py +0 -0
  91. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_server_hosts.py +0 -0
  92. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_shape_refusals.py +0 -0
  93. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_stats_not_backtests.py +0 -0
  94. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_stats_values.py +0 -0
  95. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_surfaces.py +0 -0
  96. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_telemetry_stream.py +0 -0
  97. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_ui_app.py +0 -0
  98. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_ui_engine.py +0 -0
  99. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_uncorrected_effect.py +0 -0
  100. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_unnamed_inputs.py +0 -0
  101. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_versions_not_figures.py +0 -0
  102. {hypgraph-0.7.8 → hypgraph-0.7.10}/tests/test_vocabulary_friction.py +0 -0
  103. {hypgraph-0.7.8 → hypgraph-0.7.10}/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,47 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.10 — 2026-10-30: fixes from the first live blind run
4
+
5
+ Claude led a question on a HypMesh and issued a blind order; Codex took the order through its seat and counted the
6
+ primes below 1,000,000 whose digits sum to 23 with its own sieve. The record marked its result written blind and
7
+ agreeing blind with Claude's, and nothing sealed reached Codex. Four fixes from that run and a phone-width check:
8
+
9
+ - **Investigations are part of the problem statement a blind checker reads.** Only the order's own investigation
10
+ and the curators' were. A lead whose framing lived in a second investigation had it sealed, the numbers that
11
+ define the problem (1,000,000; 23) read as secrets, and an honest blind order was refused. Questions and
12
+ investigations say what is being found out; don't put answers in them.
13
+ - **`my_work` without an investigation lists your orders in every investigation.** It asked "which
14
+ investigation?" when a record held two, and the seat's first call failed.
15
+ - **`produced_by`, `generated_by`, `output_of`, `executed_by` and `tested_by` are read as the relation they
16
+ invert.** The node types fix the direction, so the link is turned round. Codex's first result was refused for
17
+ `produced_by`.
18
+ - **On a phone, every tab of the embedded app can be reached.** The embed's `overflow: visible` beat the narrow
19
+ rule that makes the nav a scrolling strip. On hypdmz.com at 375 px, four of the eight tabs (Evidence, Work
20
+ orders, Audit log, Report) sat past the edge with no way to scroll to them. The nav now scrolls, and the embed
21
+ clips sideways, so the closed drawer can't widen a host page that doesn't clip it.
22
+
23
+ ## 0.7.9 — 2026-10-30: blind work orders
24
+
25
+ - **A checker can measure without seeing the answer it checks.** `issue_work(..., blind=True)` (MCP `issue_work`
26
+ `blind=true`, HTTP write `blind`) issues a work order whose assignee works blind until it returns the order.
27
+ The order text gives the objective, the method and the target by code only. Hypotheses and their predictions,
28
+ results, claims, reviews, runs and their code, sources, other orders and other teams' attempts are sealed.
29
+ `hypgraph.blind` holds the rules:
30
+ - **At issue**, a blind order is refused with a prediction, with a predicate that has to read its target
31
+ (`reviewed`, `no_open_objection`, `report_clean`), for an assignee that has already written in the record, and
32
+ when its objective, method or note quote a number only the sealed material carries.
33
+ - **At read**, the MCP server seals its seat while that seat holds an open blind order. The tools that summarise
34
+ what others wrote are refused, `show` refuses a sealed node, `find` leaves sealed nodes out, and every number
35
+ only the sealed material carries is scrubbed from what comes back. `blind.dispatch(g, ..., keep=)` gives a host
36
+ the same redaction for its HTTP routes, per reader (HypMesh uses it). `hypgraph serve` and the Python API read
37
+ the whole record: a file is only as blind as whoever holds it.
38
+ - **In standing**, a result its assignee wrote inside the window is `blind` (and `sealed` while the window is
39
+ open). After the return, a blind result that repeats an earlier result's numbers `agrees_blind` with it
40
+ instead of restating it, and a claim or hypothesis counts its `blind_lines`: "2 independent lines of support
41
+ (1 written blind)". A result revised after the window closed is no longer blind.
42
+ - The record sees writes, not reads. A seat that read the record before its order was issued has already seen
43
+ what is now sealed: give a checker its order first.
44
+
3
45
  ## 0.7.8 — 2026-10-29: pinned references between records
4
46
 
5
47
  - **A record can cite a node of another record and prove what it read.** A `source` takes four more fields,
@@ -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.8
3
+ Version: 0.7.10
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
@@ -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,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.8"
19
+ __version__ = "0.7.10"
20
20
 
21
21
  from .bundle import verify_bundle # noqa: E402
22
22
  from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
@@ -0,0 +1,415 @@
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 investigations and questions: what is being found out, never what
6
+ was found) 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
+ # The problem statement every reader keeps. An investigation says what is being found out; a lead whose framing
43
+ # lived in a second investigation had it sealed, and the numbers that define the problem (1,000,000; 23) then read
44
+ # as secrets, so an honest blind order was refused (first live run, 2026-10-30). Don't put answers in them.
45
+ VISIBLE_TYPES = frozenset({"question", "investigation"})
46
+ # A blind order asks for a measurement. A review or a report has to read what it judges.
47
+ BLIND_PREDICATES = frozenset({"run_registered", "result_exists", "claim_made", "within_budget"})
48
+ SEALED = "sealed"
49
+
50
+
51
+ def canon(tok: str) -> str:
52
+ """A number token as compared: no thousands separators, no trailing zeros after the point."""
53
+ t = tok.replace(",", "")
54
+ if "." in t:
55
+ t = t.rstrip("0").rstrip(".")
56
+ return t or "0"
57
+
58
+
59
+ def _significant(t: str) -> bool:
60
+ """Small whole numbers (0–12) are everywhere — counts, steps, versions — and say nothing on their own."""
61
+ try:
62
+ f = float(t)
63
+ except ValueError:
64
+ return False
65
+ return not (f.is_integer() and abs(f) <= 12)
66
+
67
+
68
+ def numbers(x: Any) -> set[str]:
69
+ """Every significant number in a body, from its text and its values."""
70
+ out: set[str] = set()
71
+
72
+ def walk(v: Any) -> None:
73
+ if isinstance(v, bool) or v is None:
74
+ return
75
+ if isinstance(v, (int, float)):
76
+ t = canon(repr(float(v))) if isinstance(v, float) else str(v)
77
+ if _significant(t):
78
+ out.add(t)
79
+ elif isinstance(v, str):
80
+ out.update(c for c in (canon(m.group(0)) for m in _NUM.finditer(v)) if _significant(c))
81
+ elif isinstance(v, dict):
82
+ for w in v.values():
83
+ walk(w)
84
+ elif isinstance(v, (list, tuple)):
85
+ for w in v:
86
+ walk(w)
87
+ walk(x)
88
+ return out
89
+
90
+
91
+ # ── windows: when an order's assignee works blind ──────────────────
92
+ def _is_order(s: D.Snap, i: str) -> bool:
93
+ n = s.nodes.get(i)
94
+ return bool(n and n["type"] == "task" and n["body"].get("assignee") and n["body"].get("objective"))
95
+
96
+
97
+ def _create_seqs(s: D.Snap) -> dict[str, int]:
98
+ return {e["ref"]: e["seq"] for e in s.events if e["kind"] == "create"}
99
+
100
+
101
+ @dataclass
102
+ class Window:
103
+ root: str # the first order of the chain (a rework keeps its window)
104
+ orders: list[str] # every order of the chain, oldest first
105
+ assignee: str
106
+ inv: str
107
+ start: int # seq the root was created at
108
+ end: int | None # seq of the first release (return or cancel) or withdrawal in the chain
109
+ sealing: bool # the window is open and the chain's current order is open or overdue
110
+ fresh: bool # the assignee had written nothing in this record before the window opened
111
+
112
+ def code(self, s: D.Snap) -> str:
113
+ return s.nodes[self.orders[-1]]["code"]
114
+
115
+
116
+ def windows(s: D.Snap) -> list[Window]:
117
+ """Every blind order chain in the record, with its window. Cached on the snapshot by its head."""
118
+ hit = getattr(s, "_blind_windows", None)
119
+ if hit is not None and hit[0] == s.seq:
120
+ return hit[1]
121
+ from . import coord
122
+ roots: dict[str, list[str]] = {}
123
+ for i, n in s.nodes.items():
124
+ if _is_order(s, i) and n["body"].get("blind"):
125
+ roots.setdefault(coord.chain_root(s, i), []).append(i)
126
+ out: list[Window] = []
127
+ if roots:
128
+ created = _create_seqs(s)
129
+ first_write: dict[str, int] = {}
130
+ for e in s.events:
131
+ if e["kind"] == "create" and e["actor"] not in first_write:
132
+ first_write[e["actor"]] = e["seq"]
133
+ for root, chain in roots.items():
134
+ if not s.nodes[root]["body"].get("blind"):
135
+ continue # a rework made blind after a sighted attempt isn't blind
136
+ chain = sorted(set(chain) | {root}, key=lambda o: created.get(o, 0))
137
+ members = set(chain)
138
+ start = created.get(root, 0)
139
+ end = None
140
+ for e in s.events:
141
+ if e["seq"] <= start:
142
+ continue
143
+ if (e["kind"] == "lease_release" and (e["payload"] or {}).get("task") in members) or \
144
+ (e["kind"] == "withdraw" and e["ref"] in members):
145
+ end = e["seq"]
146
+ break
147
+ who = s.nodes[root]["body"]["assignee"]
148
+ cur = chain[-1]
149
+ status = coord.order_status(s, cur)
150
+ out.append(Window(root, chain, who, s.nodes[root]["inv"], start, end,
151
+ sealing=end is None and status in ("open", "overdue"),
152
+ fresh=first_write.get(who, start + 1) > start))
153
+ s._blind_windows = (s.seq, out)
154
+ return out
155
+
156
+
157
+ def written_blind(s: D.Snap) -> dict[str, Window]:
158
+ """Results the assignee of a blind order wrote inside its window, unrevised since it closed. An assignee that
159
+ had written in the record before the order is not fresh: nothing it writes counts as blind."""
160
+ hit = getattr(s, "_blind_results", None)
161
+ if hit is not None and hit[0] == s.seq:
162
+ return hit[1]
163
+ out: dict[str, Window] = {}
164
+ ws = [w for w in windows(s) if w.fresh]
165
+ if ws:
166
+ created = _create_seqs(s)
167
+ last_rev: dict[str, int] = {}
168
+ for e in s.events:
169
+ if e["kind"] == "revise":
170
+ last_rev[e["ref"]] = e["seq"]
171
+ for i, n in s.nodes.items():
172
+ if n["type"] != "result":
173
+ continue
174
+ c = created.get(i, 0)
175
+ for w in ws:
176
+ if n["created_by"] == w.assignee and c > w.start and (w.end is None or c < w.end) and \
177
+ (w.end is None or last_rev.get(i, 0) < w.end):
178
+ out[i] = w
179
+ break
180
+ s._blind_results = (s.seq, out)
181
+ return out
182
+
183
+
184
+ def blind_state(s: D.Snap, nid: str) -> dict:
185
+ """What derive.state adds for blindness: on a result, the order it was written blind under, and once the window
186
+ closed, the earlier results it agrees with (instead of restating them); on a claim or hypothesis, how many of
187
+ its independent lines contain a blind result whose window has closed."""
188
+ n = s.nodes[nid]
189
+ out: dict = {}
190
+ wb = written_blind(s)
191
+ if n["type"] == "result" and nid in wb:
192
+ w = wb[nid]
193
+ out["blind"] = w.code(s)
194
+ if w.end is None:
195
+ out["sealed"] = True
196
+ if n["type"] in ("claim", "hypothesis") and wb:
197
+ closed = {r for r, w in wb.items() if w.end is not None}
198
+ if closed:
199
+ k = _blind_lines(s, nid, closed)
200
+ if k:
201
+ out["blind_lines"] = k
202
+ return out
203
+
204
+
205
+ def _blind_lines(s: D.Snap, nid: str, blind: set[str]) -> int:
206
+ lines = [e["src"] for e in D.supporters(s, nid) if not D.fallen(s, e["src"])]
207
+ groups: list[tuple[set, bool]] = []
208
+ for x in lines:
209
+ r = D.roots(s, x)
210
+ is_blind = x in blind or bool(r & {D.producing_run(s, b) for b in blind if D.producing_run(s, b)}) \
211
+ or any(b in blind for b in _derived(s, x))
212
+ merged = [g for g in groups if g[0] & r]
213
+ for g in merged:
214
+ groups.remove(g)
215
+ r = r | g[0]
216
+ is_blind = is_blind or g[1]
217
+ groups.append((r, is_blind))
218
+ return sum(1 for _r, b in groups if b)
219
+
220
+
221
+ def _derived(s: D.Snap, x: str, limit: int = 60) -> set[str]:
222
+ out, stack = set(), [x]
223
+ while stack and len(out) < limit:
224
+ y = stack.pop()
225
+ for e in s.out.get(y, []):
226
+ if e["rel"] == "derived_from" and e["dst"] not in out:
227
+ out.add(e["dst"])
228
+ stack.append(e["dst"])
229
+ return out
230
+
231
+
232
+ # ── what is sealed, and from whom ──────────────────────────────────
233
+ def public(s: D.Snap, n: dict, w: Window) -> bool:
234
+ """The problem statement every reader keeps: questions and investigations (the order chain is kept apart)."""
235
+ return n["type"] in VISIBLE_TYPES
236
+
237
+
238
+ def scope(s: D.Snap, w: Window) -> set[str]:
239
+ """What a sealing window hides from its assignee: everything other authors wrote that isn't the problem
240
+ statement or the order chain."""
241
+ chain = set(w.orders)
242
+ return {i for i, n in s.nodes.items()
243
+ if i not in chain and n["created_by"] != w.assignee and not public(s, n | {"id": i}, w)}
244
+
245
+
246
+ @dataclass
247
+ class View:
248
+ """The redaction for one reader. Empty (``bool(view) is False``) when nothing is sealed from it."""
249
+ hide: dict[str, str] = field(default_factory=dict) # node id → the order code that seals it
250
+ secrets: frozenset = frozenset() # numbers only the sealed material carries
251
+
252
+ def __bool__(self) -> bool:
253
+ return bool(self.hide)
254
+
255
+ def because(self) -> str:
256
+ codes = sorted(set(self.hide.values()))
257
+ return ", ".join(codes)
258
+
259
+ def message(self, what: str = "this") -> str:
260
+ return (f"{what} is sealed while blind work order {self.because()} is open: the record is hidden from its "
261
+ "checker until the order is returned. Work from the order (my_work) and return it.")
262
+
263
+ def allows(self, method: str, head: str) -> bool:
264
+ return head in (POST_ROUTES if method == "POST" else READ_ROUTES)
265
+
266
+ def scrub(self, x: Any) -> Any:
267
+ """Every number only the sealed material carries, replaced in every string."""
268
+ if not self.secrets:
269
+ return x
270
+ if isinstance(x, str):
271
+ return _NUM.sub(lambda m: "[sealed]" if canon(m.group(0)) in self.secrets else m.group(0), x)
272
+ if isinstance(x, dict):
273
+ return {k: self.scrub(v) for k, v in x.items()}
274
+ if isinstance(x, list):
275
+ return [self.scrub(v) for v in x]
276
+ return x
277
+
278
+ def node(self, d: dict) -> dict:
279
+ """A sealed node as a reader sees it: what it is and where, not what it says."""
280
+ code = self.hide.get(d.get("id"), "")
281
+ st = d.get("state") or {}
282
+ out = {k: d[k] for k in ("id", "type", "code", "inv", "v", "by", "at") if k in d}
283
+ out |= {"title": f"sealed until {code} is returned", "kind": "", SEALED: code,
284
+ "state": {"lifecycle": st.get("lifecycle", "active"), SEALED: code,
285
+ "sentence": f"sealed until blind work order {code} is returned"}}
286
+ if "body" in d:
287
+ out["body"] = {SEALED: f"until {code} is returned"}
288
+ for k in ("out", "in"):
289
+ if k in d:
290
+ out[k] = [self.edge(e) for e in d[k]]
291
+ return out
292
+
293
+ def edge(self, e: dict) -> dict:
294
+ if e.get("src") in self.hide or e.get("dst") in self.hide:
295
+ return e | {"why": "", "data": {}}
296
+ return e
297
+
298
+ def event(self, e: dict) -> dict:
299
+ if e.get("ref") in self.hide:
300
+ p = e.get("payload") or {}
301
+ return e | {"payload": {k: p[k] for k in ("type", "code", "inv", "v", "task") if k in p}}
302
+ return e
303
+
304
+ def apply(self, x: Any) -> Any:
305
+ """Redact a response: sealed nodes become placeholders, edges touching them lose their reasons, events on
306
+ them lose their payload, and secret numbers are scrubbed everywhere."""
307
+ if not self:
308
+ return x
309
+ return self.scrub(self._walk(x))
310
+
311
+ def _walk(self, x: Any) -> Any:
312
+ if isinstance(x, list):
313
+ return [self._walk(v) for v in x]
314
+ if not isinstance(x, dict):
315
+ return x
316
+ if x.get("id") in self.hide and ("type" in x or "code" in x or "body" in x):
317
+ return self.node(x)
318
+ if "src" in x and "dst" in x and "rel" in x:
319
+ return self.edge(x)
320
+ if "seq" in x and "kind" in x and "ref" in x:
321
+ return self.event(x)
322
+ return {k: self._walk(v) for k, v in x.items()}
323
+
324
+ def search(self, rows: list[dict]) -> list[dict]:
325
+ """Search results without sealed nodes: a hit on a sealed body says what the body contains."""
326
+ return [r for r in rows if r.get("id") not in self.hide]
327
+
328
+
329
+ def view_for(s: D.Snap, keep: Callable[[str, dict], bool]) -> View:
330
+ """The redaction for a reader, while any blind order is sealing: every node in a sealing window's scope, except
331
+ those ``keep(node_id, node)`` says this reader may see (its own). Secret numbers are the ones in hidden bodies
332
+ that no visible node carries."""
333
+ hide: dict[str, str] = {}
334
+ for w in windows(s):
335
+ if not w.sealing:
336
+ continue
337
+ code = w.code(s)
338
+ for i in scope(s, w):
339
+ if i not in hide and not keep(i, s.nodes[i]):
340
+ hide[i] = code
341
+ if not hide:
342
+ return View()
343
+ hidden_nums: set[str] = set()
344
+ seen_nums: set[str] = set()
345
+ for i, n in s.nodes.items():
346
+ (hidden_nums if i in hide else seen_nums).update(numbers(n["body"]))
347
+ return View(hide, frozenset(hidden_nums - seen_nums))
348
+
349
+
350
+ def view_for_author(s: D.Snap, author: str) -> View:
351
+ """The redaction for an MCP seat writing as ``author``: sealed only while it holds an open blind order, and then
352
+ everything in that order's scope is hidden from it."""
353
+ mine = [w for w in windows(s) if w.sealing and w.assignee == author]
354
+ if not mine:
355
+ return View()
356
+ hide: dict[str, str] = {}
357
+ for w in mine:
358
+ for i in scope(s, w):
359
+ hide.setdefault(i, w.code(s))
360
+ hidden_nums: set[str] = set()
361
+ seen_nums: set[str] = set()
362
+ for i, n in s.nodes.items():
363
+ (hidden_nums if i in hide else seen_nums).update(numbers(n["body"]))
364
+ return View(hide, frozenset(hidden_nums - seen_nums))
365
+
366
+
367
+ def dispatch(g, method: str, path: str, *, query: dict | None = None, body: dict | None = None,
368
+ keep: Callable[[str, dict], bool] = lambda i, n: False, **kw) -> tuple[int, Any]:
369
+ """``server.dispatch`` for a reader while blind orders may be open: refused where a route would quote sealed
370
+ material, redacted everywhere else. ``keep`` says which nodes this reader may see anyway (its own)."""
371
+ from .server import dispatch as plain
372
+ s = g.snapshot()
373
+ if not windows(s):
374
+ return plain(g, method, path, query=query, body=body, **kw)
375
+ v = view_for(s, keep)
376
+ if not v:
377
+ return plain(g, method, path, query=query, body=body, **kw)
378
+ parts = [p for p in path.strip("/").split("/") if p]
379
+ head, arg = (parts[0] if parts else ""), (parts[1] if len(parts) > 1 else "")
380
+ if method == "POST" and head == "write":
381
+ status, out = plain(g, method, path, query=query, body=body, **kw)
382
+ return status, v.apply(out)
383
+ if not v.allows(method, head):
384
+ return 409, {"detail": v.message(f"/{head}"), SEALED: v.because()}
385
+ if head in ("order", "node", "why", "impact", "neighborhood") and arg:
386
+ try:
387
+ nid = g.resolve(arg)
388
+ except Exception:
389
+ nid = arg
390
+ if head == "order" and nid in v.hide:
391
+ return 409, {"detail": v.message(f"order {arg}"), SEALED: v.because()}
392
+ status, out = plain(g, method, path, query=query, body=body, **kw)
393
+ if head == "search" and status == 200 and isinstance(out, dict) and isinstance(out.get("results"), list):
394
+ out = out | {"results": v.search(out["results"])}
395
+ return status, v.apply(out)
396
+
397
+
398
+ def leaks(s: D.Snap, w: Window, texts: list[str]) -> dict[str, list[str]]:
399
+ """Numbers an order's own text quotes that only the material it would seal carries: number → codes holding it."""
400
+ hide = scope(s, w)
401
+ visible: set[str] = set()
402
+ for i, n in s.nodes.items():
403
+ if i not in hide:
404
+ visible |= numbers(n["body"])
405
+ quoted = numbers(texts) - visible
406
+ out: dict[str, list[str]] = {}
407
+ for i in hide:
408
+ hit = quoted & numbers(s.nodes[i]["body"])
409
+ for x in hit:
410
+ out.setdefault(x, []).append(s.nodes[i]["code"] or i)
411
+ return {k: sorted(v) for k, v in out.items()}
412
+
413
+
414
+ __all__ = ["Window", "View", "windows", "written_blind", "blind_state", "scope", "view_for", "view_for_author",
415
+ "dispatch", "leaks", "numbers", "BLIND_PREDICATES"]