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.
- {hypgraph-0.7.7 → hypgraph-0.7.9}/AGENTS.md +2 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/CHANGELOG.md +35 -0
- hypgraph-0.7.9/DISCLAIMER.md +36 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/PKG-INFO +7 -1
- {hypgraph-0.7.7 → hypgraph-0.7.9}/README.md +6 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/spec.md +8 -1
- {hypgraph-0.7.7 → hypgraph-0.7.9}/pyproject.toml +1 -1
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/__init__.py +3 -2
- hypgraph-0.7.9/src/hypgraph/blind.py +414 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/coord.py +79 -2
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/derive.py +15 -1
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/graph.py +26 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/mcp.py +88 -40
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/plan.py +2 -1
- hypgraph-0.7.9/src/hypgraph/references.py +173 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/schema.py +7 -3
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/server.py +12 -1
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/skill/SKILL.md +3 -0
- hypgraph-0.7.9/tests/test_blind.py +168 -0
- hypgraph-0.7.9/tests/test_references.py +156 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/.gitignore +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/CITATION.cff +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/LICENSE +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/NOTICE +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/PLAN.md +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/decisions/ADR-002-governance-profiles.md +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/profile-workflows-plan.md +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/review-2026-10-24.md +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/site-plan.md +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/ui-plan.md +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/workflows/README.md +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/workflows/knowledge.md +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/workflows/optimization.md +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/workflows/scientific.md +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/workflows/software.md +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/docs/workflows/trading.md +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/__main__.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/bundle.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/checks.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/cli.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/demos/__init__.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/demos/broad_street.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/language.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/policy.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/profiles.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/refusals.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/report_check.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/stats.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/store.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/__init__.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/atlas.css +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/atlas.js +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/hypgraph.css +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/hypgraph.js +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/hypgraph.svg +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/index.html +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/vendor/cose-base.js +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/ui/vendor/layout-base.js +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/src/hypgraph/workflow.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_accept_work_kind.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_bound_support.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_bundle_telemetry.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_cancel_releases_lease.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_cited_and_failed_support.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_coord.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_d10.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_engine.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_engine_fixes.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_incremental.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_link_translate.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_log_and_check.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_measure_questions.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_opposed_answers.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_option_lead.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_order_detail.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_profiles.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_profiles_041.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_questions_not_assertions.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_refusal_friction.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_refusal_results.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_refusals.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_relation_words.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_report_answers.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_rerunnable.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_restates_scale.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_server_auth.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_server_hosts.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_shape_refusals.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_stats_not_backtests.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_stats_values.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_surfaces.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_telemetry_stream.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_ui_app.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_ui_engine.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_uncorrected_effect.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_unnamed_inputs.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_versions_not_figures.py +0 -0
- {hypgraph-0.7.7 → hypgraph-0.7.9}/tests/test_vocabulary_friction.py +0 -0
- {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.
|
|
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.
|
|
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"]
|