hypgraph 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hypgraph-0.2.0 → hypgraph-0.3.0}/AGENTS.md +3 -2
- {hypgraph-0.2.0 → hypgraph-0.3.0}/CHANGELOG.md +21 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/CITATION.cff +1 -1
- {hypgraph-0.2.0 → hypgraph-0.3.0}/PKG-INFO +29 -2
- {hypgraph-0.2.0 → hypgraph-0.3.0}/README.md +28 -1
- {hypgraph-0.2.0 → hypgraph-0.3.0}/docs/spec.md +38 -2
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/__init__.py +1 -1
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/checks.py +14 -10
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/cli.py +39 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/graph.py +41 -6
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/language.py +1 -1
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/mcp.py +28 -1
- hypgraph-0.3.0/src/hypgraph/refusals.py +182 -0
- hypgraph-0.3.0/src/hypgraph/report_check.py +271 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/server.py +7 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/skill/SKILL.md +13 -1
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/ui/atlas.js +22 -0
- hypgraph-0.3.0/tests/test_log_and_check.py +205 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/tests/test_surfaces.py +2 -1
- {hypgraph-0.2.0 → hypgraph-0.3.0}/.gitignore +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/LICENSE +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/NOTICE +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/docs/PLAN.md +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/pyproject.toml +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/__main__.py +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/bundle.py +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/demos/__init__.py +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/demos/broad_street.py +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/derive.py +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/plan.py +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/policy.py +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/schema.py +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/stats.py +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/store.py +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/ui/__init__.py +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/ui/atlas.css +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/ui/hypgraph.svg +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/ui/index.html +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/ui/vendor/cose-base.js +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/src/hypgraph/ui/vendor/layout-base.js +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/tests/test_engine.py +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/tests/test_incremental.py +0 -0
- {hypgraph-0.2.0 → hypgraph-0.3.0}/tests/test_refusals.py +0 -0
|
@@ -65,10 +65,11 @@ reserved.
|
|
|
65
65
|
are not;
|
|
66
66
|
- `kind: "exclusion"` needs a result with `power: "demonstrated"` (a search that found a planted answer).
|
|
67
67
|
Otherwise use `"inconclusive"`.
|
|
68
|
-
Each refusal ends in a code (`[ABSOLUTE_UNDERPOWERED]`) and already gives the honest sentence: use it.
|
|
68
|
+
Each refusal ends in a code (`[ABSOLUTE_UNDERPOWERED]`) and already gives the honest sentence: use it. Refusals
|
|
69
|
+
are logged per author (`refusal_log`); coming back with the honest version marks yours fixed.
|
|
69
70
|
6. **Reviews.** Only reviewers named by the investigation (or the host's policy) set standing. Nobody reviews their
|
|
70
71
|
own work. A revision makes earlier reviews stale.
|
|
71
|
-
7. **Start summaries from `report()
|
|
72
|
+
7. **Start summaries from `report()`, and run `check_report(text)` on the write-up before handing it over.** It's generated from the record, so it can't restate a withdrawn claim. Write
|
|
72
73
|
your interpretation underneath it.
|
|
73
74
|
|
|
74
75
|
## Reading standing
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 — 2026-10-24
|
|
4
|
+
|
|
5
|
+
- **The refusal log.** Every refused claim is logged: author, statement, codes, messages. A later accepted claim by
|
|
6
|
+
the same author that says the same thing (same node, or ≥ 30% shared content words within 24 hours) marks the
|
|
7
|
+
refusal fixed. Append-only, with its own hash chain (`verify_refusals()`); the record's chain is untouched.
|
|
8
|
+
- API: `refusals()`, `refusal_report()`.
|
|
9
|
+
- CLI: `hypgraph refusals [--author …]`. MCP: `refusal_log`. HTTP: `GET /api/graph/refusals`.
|
|
10
|
+
- Atlas: an "Overclaims caught" card in the Work view.
|
|
11
|
+
- **`hypgraph check report.md`** lints a write-up against the record (`Graph.check_text`; MCP `check_report`; HTTP
|
|
12
|
+
`POST /api/graph/check`). Exits 1 on errors (`--strict`: warnings too).
|
|
13
|
+
- Errors: citations of withdrawn nodes, settled language about unchecked, contested or at-risk work, unknown
|
|
14
|
+
codes, and unbacked never / no effect / significantly.
|
|
15
|
+
- Warnings: numbers no result contains or implies, omitted disputes, superseded citations, uncited causal
|
|
16
|
+
language.
|
|
17
|
+
- Calibrated on 24 real PegasusDMZ write-ups against its live record. That fixed negations ("cannot be proven"),
|
|
18
|
+
quoted passages under correction, instructions ("never use the eval set"), p-values given in the sentence, and
|
|
19
|
+
"results in" as a noun phrase.
|
|
20
|
+
- An absolute whose statement gives its n ("no leaks in 50 prompts", "nothing in 50 of 50") is scoped to what was
|
|
21
|
+
seen and accepted; its standing still carries the bound. Without n it is refused, as before.
|
|
22
|
+
- Spec draft 3 (§8 refusal log, §9 checking a write-up).
|
|
23
|
+
|
|
3
24
|
## 0.2.0 — 2026-10-24
|
|
4
25
|
|
|
5
26
|
**Refusal, remastered.** The claim checks now do the arithmetic the writer skipped, and every refusal says what the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hypgraph
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: The evidence layer for AI research agents: a hash-chained research record that checks every claim as it is written and derives how well each is known.
|
|
5
5
|
Project-URL: Homepage, https://hypgraph.com
|
|
6
6
|
Project-URL: Documentation, https://hypgraph.com/engineers/
|
|
@@ -82,6 +82,33 @@ Every refusal has a stable code, the arithmetic, and the sentence that *would* b
|
|
|
82
82
|
claim in one step. `hypgraph bound 0 50 --exposure 1e6` (or the MCP tool `bound`) answers "what does my count
|
|
83
83
|
actually support?" before anyone writes the word.
|
|
84
84
|
|
|
85
|
+
Every refusal is also **logged**: who tried to say what, which rules caught it, and whether they came back with
|
|
86
|
+
the honest version.
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
$ hypgraph refusals
|
|
90
|
+
## Overclaims caught — 4 refused, 3 came back honest
|
|
91
|
+
| author | refused | fixed | open | most common |
|
|
92
|
+
| analyst | 4 | 3 | 1 | EXCLUSION_WITHOUT_POWER ×1, ABSOLUTE_UNDERPOWERED ×1, UNBOUND_NUMBER ×1 |
|
|
93
|
+
### Latest
|
|
94
|
+
- analyst: “Prompt format makes no difference to accuracy.” — EXCLUSION_WITHOUT_POWER → not fixed
|
|
95
|
+
- analyst: “The model never leaks training data.” — ABSOLUTE_UNDERPOWERED → fixed as CL2
|
|
96
|
+
- analyst: “The fine-tune is 5 points better than the baseline.” — UNBOUND_NUMBER → fixed as CL1
|
|
97
|
+
- analyst: “The fine-tune always beats the baseline.” — ABSOLUTE_WITHOUT_COUNT, COMPARATIVE_WITHOUT_COMPARISON → fixed as CL1
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
And because most hype lives in the write-up, not the record, **`hypgraph check summary.md`** lints the final report
|
|
101
|
+
against the record. It catches numbers no result contains, claims called "established" that the record has as
|
|
102
|
+
unchecked or contested, citations to withdrawn claims, and "never" / "no effect" / "significantly" with nothing
|
|
103
|
+
behind them. It exits 1 on errors, so it can gate CI:
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
$ hypgraph check summary.md
|
|
107
|
+
summary.md:8: error REPORT_OVERSTATES_STANDING: 'established' — but CL2 is unchecked in the record …
|
|
108
|
+
summary.md:9: warning REPORT_UNSOURCED_NUMBER: 700 isn't in any result, measurement, claim, scope or constant …
|
|
109
|
+
summary.md:13: error REPORT_UNKNOWN_CODE: CL9 isn't in the record for this investigation — a citation to nothing.
|
|
110
|
+
```
|
|
111
|
+
|
|
85
112
|
And when something does fall, the record says what falls with it:
|
|
86
113
|
|
|
87
114
|
```text
|
|
@@ -116,7 +143,7 @@ hypgraph skill --install # +
|
|
|
116
143
|
The server writes as one author, fixed when it starts. The agent can't pass another name or write as your job runner,
|
|
117
144
|
and its verdicts count only if you name it as a reviewer. Tools: `overview`, `find`, `show`, `start_investigation`,
|
|
118
145
|
`add`, `link`, `revise`, `withdraw`, `retract_link`, `review`, `next_experiments`, `work_queue`, `claim_work`,
|
|
119
|
-
`register_run`, `bound`, `report`, `verify`, `schema`. The method is in [AGENTS.md](https://github.com/voodoomods/hypgraph/blob/main/AGENTS.md); the short version is
|
|
146
|
+
`register_run`, `bound`, `check_report`, `refusal_log`, `report`, `verify`, `schema`. The method is in [AGENTS.md](https://github.com/voodoomods/hypgraph/blob/main/AGENTS.md); the short version is
|
|
120
147
|
[llms.txt](https://hypgraph.com/llms.txt).
|
|
121
148
|
|
|
122
149
|
## Wire it into your system
|
|
@@ -50,6 +50,33 @@ Every refusal has a stable code, the arithmetic, and the sentence that *would* b
|
|
|
50
50
|
claim in one step. `hypgraph bound 0 50 --exposure 1e6` (or the MCP tool `bound`) answers "what does my count
|
|
51
51
|
actually support?" before anyone writes the word.
|
|
52
52
|
|
|
53
|
+
Every refusal is also **logged**: who tried to say what, which rules caught it, and whether they came back with
|
|
54
|
+
the honest version.
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
$ hypgraph refusals
|
|
58
|
+
## Overclaims caught — 4 refused, 3 came back honest
|
|
59
|
+
| author | refused | fixed | open | most common |
|
|
60
|
+
| analyst | 4 | 3 | 1 | EXCLUSION_WITHOUT_POWER ×1, ABSOLUTE_UNDERPOWERED ×1, UNBOUND_NUMBER ×1 |
|
|
61
|
+
### Latest
|
|
62
|
+
- analyst: “Prompt format makes no difference to accuracy.” — EXCLUSION_WITHOUT_POWER → not fixed
|
|
63
|
+
- analyst: “The model never leaks training data.” — ABSOLUTE_UNDERPOWERED → fixed as CL2
|
|
64
|
+
- analyst: “The fine-tune is 5 points better than the baseline.” — UNBOUND_NUMBER → fixed as CL1
|
|
65
|
+
- analyst: “The fine-tune always beats the baseline.” — ABSOLUTE_WITHOUT_COUNT, COMPARATIVE_WITHOUT_COMPARISON → fixed as CL1
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
And because most hype lives in the write-up, not the record, **`hypgraph check summary.md`** lints the final report
|
|
69
|
+
against the record. It catches numbers no result contains, claims called "established" that the record has as
|
|
70
|
+
unchecked or contested, citations to withdrawn claims, and "never" / "no effect" / "significantly" with nothing
|
|
71
|
+
behind them. It exits 1 on errors, so it can gate CI:
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
$ hypgraph check summary.md
|
|
75
|
+
summary.md:8: error REPORT_OVERSTATES_STANDING: 'established' — but CL2 is unchecked in the record …
|
|
76
|
+
summary.md:9: warning REPORT_UNSOURCED_NUMBER: 700 isn't in any result, measurement, claim, scope or constant …
|
|
77
|
+
summary.md:13: error REPORT_UNKNOWN_CODE: CL9 isn't in the record for this investigation — a citation to nothing.
|
|
78
|
+
```
|
|
79
|
+
|
|
53
80
|
And when something does fall, the record says what falls with it:
|
|
54
81
|
|
|
55
82
|
```text
|
|
@@ -84,7 +111,7 @@ hypgraph skill --install # +
|
|
|
84
111
|
The server writes as one author, fixed when it starts. The agent can't pass another name or write as your job runner,
|
|
85
112
|
and its verdicts count only if you name it as a reviewer. Tools: `overview`, `find`, `show`, `start_investigation`,
|
|
86
113
|
`add`, `link`, `revise`, `withdraw`, `retract_link`, `review`, `next_experiments`, `work_queue`, `claim_work`,
|
|
87
|
-
`register_run`, `bound`, `report`, `verify`, `schema`. The method is in [AGENTS.md](https://github.com/voodoomods/hypgraph/blob/main/AGENTS.md); the short version is
|
|
114
|
+
`register_run`, `bound`, `check_report`, `refusal_log`, `report`, `verify`, `schema`. The method is in [AGENTS.md](https://github.com/voodoomods/hypgraph/blob/main/AGENTS.md); the short version is
|
|
88
115
|
[llms.txt](https://hypgraph.com/llms.txt).
|
|
89
116
|
|
|
90
117
|
## Wire it into your system
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# The HypGraph record format — `hypgraph/1`
|
|
2
2
|
|
|
3
|
-
Status: draft
|
|
3
|
+
Status: draft 3 (HypGraph 0.3). Licence: CC BY 4.0. This page is enough to write another implementation, a
|
|
4
4
|
verifier or a viewer.
|
|
5
5
|
|
|
6
6
|
## 1. Model
|
|
@@ -175,7 +175,7 @@ smallest detectable difference uses 80% power at two-sided α = 0.05.
|
|
|
175
175
|
| 4 | `UNBOUND_NUMBER` | a number in the statement (digits, or the words two to twenty; digits in quotes are names) is not one of: a bound measurement's numbers; for a count, its rate, complement or n − k; for any two measurements, their difference, ratios, relative change or sum; the scope's numbers; the investigation's `constants`; or the conventions 95 / 99 / 90 / 80 / 0.05 / 0.01 when the statement speaks of intervals, bounds, confidence, power or significance. *Statistical* quantities (a rate's 95% bounds; for two counts the difference interval, p-value and detectable difference) count only when written to at least one decimal place. |
|
|
176
176
|
| 5 | `ABSOLUTE_WITHOUT_COUNT` | an absolute word (never, always, all, every, none, impossible, cannot, proves, certainly, guaranteed, eliminates, entirely, completely, universally, consistently, perfect, solves, zero, 100%, …) with no bound count where k is 0 or n |
|
|
177
177
|
| 5a | `ABSOLUTE_UNRELATED_COUNT` | the counts with k ∈ {0, n} share no content word with the statement in their name or key |
|
|
178
|
-
| 5b | `ABSOLUTE_UNDERPOWERED` | the 95% bound on the opposite rate is above 5
|
|
178
|
+
| 5b | `ABSOLUTE_UNDERPOWERED` | the 95% bound on the opposite rate is above 5% and the statement doesn't say n ("never leaks" rather than "no leaks in 50 prompts" — a statement that gives its n is scoped to what was seen, and accepted). The message gives the honest wording and the trials needed for a 1-in-1,000 bound. |
|
|
179
179
|
| 5c | `ABSOLUTE_HIDES_DENOMINATOR` | the count is enough, but n appears in neither the statement nor the scope |
|
|
180
180
|
| 6 | `CONVENTION_HEDGED` | a `convention` hedges (could, might, suggests, consistent with, …) |
|
|
181
181
|
| 7 | `COMPARATIVE_WITHOUT_COMPARISON` | a comparative (better, beats, outperforms, higher than, improves, exceeds, …) with fewer than two measured sides, no bound p-value or interval, and no supporting claim that binds two sides |
|
|
@@ -191,3 +191,39 @@ standing carries `bounds` (per count: k, n, rate, lo, hi and, with exposure, `un
|
|
|
191
191
|
|
|
192
192
|
**Blast radius.** Withdrawing a node, or a verdict that makes it fall, returns `blast_radius`: every node resting on it
|
|
193
193
|
by type, with claims and decisions named and, if a spend meter is connected, the cost of the work behind them.
|
|
194
|
+
|
|
195
|
+
## 8. The refusal log
|
|
196
|
+
|
|
197
|
+
A refused claim never enters the record, so its trace lives in a separate append-only table, `refusal_log`, with
|
|
198
|
+
its own SHA-256 chain over `canon({at, kind, author, inv, node, op, statement, codes, detail, ref, prev})`. The
|
|
199
|
+
record's own chain and `verify()` are unaffected.
|
|
200
|
+
|
|
201
|
+
- `kind: refused`: the author, the investigation, the statement (≤ 1,500 characters), the sorted codes, and the
|
|
202
|
+
problems (code, message, fix) it was refused with. `op` is `create` or `revise`; `node` is set for a revision.
|
|
203
|
+
- `kind: fixed`: a later claim or revision by the same author in the same investigation, accepted within 24 hours,
|
|
204
|
+
that says the same thing. "The same" means the same node for a revision; otherwise a Jaccard overlap ≥ 0.3 of the
|
|
205
|
+
statements' content words. `ref` is the refusal it fixes.
|
|
206
|
+
|
|
207
|
+
A report aggregates per author (refused, fixed, open, codes) and per rule.
|
|
208
|
+
|
|
209
|
+
## 9. Checking a write-up against the record
|
|
210
|
+
|
|
211
|
+
A checker reads markdown sentence by sentence and checks it against the record. Code blocks, inline code, URLs, link
|
|
212
|
+
targets and HTML comments are skipped. Quoted passages and negated words ("cannot be proven") are not treated as
|
|
213
|
+
the writer's assertion. Codes are matched within the investigation being reported on.
|
|
214
|
+
|
|
215
|
+
| level | code | when |
|
|
216
|
+
|---|---|---|
|
|
217
|
+
| error | `REPORT_CITES_WITHDRAWN` | a sentence cites a withdrawn node and doesn't say so (withdrawn, retracted, failed, superseded, earlier, …) |
|
|
218
|
+
| error | `REPORT_OVERSTATES_STANDING` | settled language (confirmed, established, shown, proven, holds, …) about a claim or hypothesis that is unchecked, stale, contested or at risk, or a result that is contested or at risk |
|
|
219
|
+
| error | `REPORT_UNKNOWN_CODE` | an `INV` / `RUN` / `CL` / `RS` / `RV` code the investigation doesn't have |
|
|
220
|
+
| error | `REPORT_ABSOLUTE_UNBACKED` | never / always / impossible / guaranteed / eliminates / proven / 100% asserted, not in an instruction, with no cited claim carrying a count with k ∈ {0, n} |
|
|
221
|
+
| error | `REPORT_ABSENCE_UNBACKED` | "no effect" / "rules out" / "doesn't matter" without citing a claim filed as `exclusion` or `inconclusive` |
|
|
222
|
+
| error | `REPORT_SIGNIFICANCE_UNBACKED` | "significantly" without a p-value or interval in the sentence and without citing a node that carries a test (a negated "not significant" is a warning) |
|
|
223
|
+
| warning | `REPORT_UNSOURCED_NUMBER` | a number not contained in or implied by the investigation's results, live claims' measurements, statements and scopes, the investigation's scope and constants, or experiment and run settings. Integers ≤ 12, years, dates, times and 7-digit+ ids are skipped. Statistical quantities need one decimal. |
|
|
224
|
+
| warning | `REPORT_OMITS_DISPUTE` | cites a contested or at-risk node without saying so |
|
|
225
|
+
| warning | `REPORT_CITES_SUPERSEDED` | cites a superseded node without saying so |
|
|
226
|
+
| warning | `REPORT_CAUSAL_UNCITED` | causal language with no node cited |
|
|
227
|
+
| note | `REPORT_BOUND` | an absolute backed by a cited count: what the count supports |
|
|
228
|
+
|
|
229
|
+
Calibrated on 24 real write-ups from PegasusDMZ against its live record.
|
|
@@ -16,7 +16,7 @@ Standing is derived on every read, never stored; every write is an event in a SH
|
|
|
16
16
|
their numbers; reviews pin versions; independence is counted, not assumed. See https://hypgraph.com.
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
-
__version__ = "0.
|
|
19
|
+
__version__ = "0.3.0"
|
|
20
20
|
|
|
21
21
|
from .bundle import verify_bundle # noqa: E402
|
|
22
22
|
from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
|
|
@@ -13,8 +13,9 @@ and each refusal does the arithmetic the writer skipped, then offers the honest
|
|
|
13
13
|
computed from them (difference, ratio, percentage, interval), the scope or a constant
|
|
14
14
|
ABSOLUTE_WITHOUT_COUNT "never" / "always" / "eliminates" / "100%" with no count of 0 or n behind it
|
|
15
15
|
ABSOLUTE_UNRELATED_COUNT the 0-of-n it leans on counts something else
|
|
16
|
-
ABSOLUTE_UNDERPOWERED 0 of n
|
|
17
|
-
|
|
16
|
+
ABSOLUTE_UNDERPOWERED 0 of n with n too small (the rate could still be above 5%), and the statement
|
|
17
|
+
generalises past the n trials instead of saying them ("never leaks")
|
|
18
|
+
ABSOLUTE_HIDES_DENOMINATOR 0 of n is enough, but neither statement nor scope says n — "never" reads as never
|
|
18
19
|
COMPARATIVE_WITHOUT_COMPARISON "beats" / "outperforms" / "higher than" with fewer than two sides measured
|
|
19
20
|
SIGNIFICANCE_WITHOUT_TEST "significantly" with no test behind it
|
|
20
21
|
SIGNIFICANCE_NOT_SHOWN the test behind it isn't significant
|
|
@@ -211,6 +212,10 @@ def _absolutes(stmt: str, scope: str, body: dict, bound: list) -> list[Problem]:
|
|
|
211
212
|
opposite = hi if k == 0 else 1 - lo # how often the opposite could still happen
|
|
212
213
|
seen = f"{k} of {n:,}"
|
|
213
214
|
what = "it happening" if k == 0 else "it failing"
|
|
215
|
+
in_statement = n in {round(v) for v, *_ in numbers(stmt)}
|
|
216
|
+
in_scope = n in {round(v) for v, *_ in numbers(scope)}
|
|
217
|
+
if in_statement:
|
|
218
|
+
return [] # scoped to the trials it states ("nothing in 50 prompts"); its standing carries the bound
|
|
214
219
|
if opposite > UNDERPOWERED:
|
|
215
220
|
need = stats.trials_for(0.001)
|
|
216
221
|
honest = (f"'no {'occurrences' if k == 0 else 'exceptions'} in {n:,} trials (95% upper bound "
|
|
@@ -220,14 +225,13 @@ def _absolutes(stmt: str, scope: str, body: dict, bound: list) -> list[Problem]:
|
|
|
220
225
|
f"time (95% upper bound). Honest version: {honest}. To support a rate below 1 in 1,000 you "
|
|
221
226
|
f"need about {need:,} trials without one." + _exposure_note(opposite, body),
|
|
222
227
|
fix=honest.strip("'"), words=words)]
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return []
|
|
228
|
+
if in_scope:
|
|
229
|
+
return [] # enough trials, and the scope says how many
|
|
230
|
+
return [Problem("ABSOLUTE_HIDES_DENOMINATOR",
|
|
231
|
+
f"'{w}' rests on {seen} — say the {n:,} in the statement or scope, so readers can see what "
|
|
232
|
+
f"'{w}' means here: {what} no more than {stats.pct(opposite)} of the time "
|
|
233
|
+
f"({stats.one_in(opposite)}, 95% upper bound)." + _exposure_note(opposite, body),
|
|
234
|
+
fix=f"add 'in {n:,} trials'", words=words)]
|
|
231
235
|
|
|
232
236
|
|
|
233
237
|
def _measured_claim(s: Snap, support: list[str]) -> bool:
|
|
@@ -11,6 +11,8 @@ hypgraph — the command line.
|
|
|
11
11
|
hypgraph show CL1 --why what it rests on (--impact: what rests on it, --history: versions)
|
|
12
12
|
hypgraph report the facts, generated from the record
|
|
13
13
|
hypgraph export -o study.json a bundle anyone can verify: hypgraph verify study.json
|
|
14
|
+
hypgraph check summary.md lint a write-up against the record (exit 1 on errors)
|
|
15
|
+
hypgraph refusals the overclaims the record caught, per author
|
|
14
16
|
hypgraph mcp an MCP server for agents (pip install "hypgraph[mcp]")
|
|
15
17
|
hypgraph skill --install the agent skill, into .claude/skills/hypgraph/SKILL.md
|
|
16
18
|
|
|
@@ -304,6 +306,35 @@ def cmd_demo(args) -> int:
|
|
|
304
306
|
return 0
|
|
305
307
|
|
|
306
308
|
|
|
309
|
+
def cmd_refusals(args) -> int:
|
|
310
|
+
g = _graph(args)
|
|
311
|
+
inv = g.resolve(args.inv) if args.inv else ""
|
|
312
|
+
if args.author_filter:
|
|
313
|
+
rows = g.refusals(inv=inv, author=args.author_filter, since=args.since)
|
|
314
|
+
_out(args, rows, "\n".join(f"{r['at'][:16]} {', '.join(r['codes'])}\n “{r['statement'][:160]}”"
|
|
315
|
+
+ (f"\n → fixed as {r['fixed_by']['node']}" if r["fixed_by"] else "")
|
|
316
|
+
for r in rows) or "No refusals.")
|
|
317
|
+
return 0
|
|
318
|
+
r = g.refusal_report(inv=inv, since=args.since)
|
|
319
|
+
_out(args, r, r["markdown"])
|
|
320
|
+
return 0
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def cmd_check(args) -> int:
|
|
324
|
+
g = _graph(args)
|
|
325
|
+
text = Path(args.file).read_text(encoding="utf-8") if args.file != "-" else sys.stdin.read()
|
|
326
|
+
r = g.check_text(text, g.resolve(args.inv) if args.inv else "")
|
|
327
|
+
if getattr(args, "json", False):
|
|
328
|
+
_out(args, r)
|
|
329
|
+
else:
|
|
330
|
+
name = args.file
|
|
331
|
+
for f in r["findings"]:
|
|
332
|
+
print(f"{name}:{f['line']}: {f['level']} {f['code']}: {f['message']}\n “{f['excerpt']}”")
|
|
333
|
+
print(r["markdown"].splitlines()[0])
|
|
334
|
+
failed = r["errors"] > 0 or (args.strict and r["warnings"] > 0)
|
|
335
|
+
return 1 if failed else 0
|
|
336
|
+
|
|
337
|
+
|
|
307
338
|
def cmd_bound(args) -> int:
|
|
308
339
|
from .mcp import bound_text
|
|
309
340
|
from . import stats
|
|
@@ -427,6 +458,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
427
458
|
s.add_argument("bundle")
|
|
428
459
|
cmd("demo", cmd_demo, "build the Broad Street (1854) teaching investigation")
|
|
429
460
|
cmd("schema", cmd_schema, "the node types and relations")
|
|
461
|
+
s = cmd("refusals", cmd_refusals, "the overclaims the record caught: per author, per rule, fixed or open")
|
|
462
|
+
s.add_argument("--inv", default="")
|
|
463
|
+
s.add_argument("--since", default="", help="ISO date or time")
|
|
464
|
+
s.add_argument("--author", dest="author_filter", default="", help="list one author's refusals in full")
|
|
465
|
+
s = cmd("check", cmd_check, "lint a write-up (markdown) against the record; exit 1 on errors")
|
|
466
|
+
s.add_argument("file", help="the markdown file ('-' for stdin)")
|
|
467
|
+
s.add_argument("--inv", default="", help="the investigation it reports on (default: inferred)")
|
|
468
|
+
s.add_argument("--strict", action="store_true", help="warnings (e.g. unsourced numbers) fail too")
|
|
430
469
|
s = cmd("bound", cmd_bound, "what k of n actually supports: the 95% bounds behind any 'never'")
|
|
431
470
|
s.add_argument("k", type=int, help="how many times it happened")
|
|
432
471
|
s.add_argument("n", type=int, help="how many trials")
|
|
@@ -27,7 +27,7 @@ from pathlib import Path
|
|
|
27
27
|
from typing import Any, Callable, Optional
|
|
28
28
|
|
|
29
29
|
from . import derive as D
|
|
30
|
-
from . import store
|
|
30
|
+
from . import refusals, store
|
|
31
31
|
from .checks import claim_issues
|
|
32
32
|
from .policy import Policy
|
|
33
33
|
from .schema import REPLICATION, RELS, SAME_TYPE, TYPES, VERDICTS
|
|
@@ -138,6 +138,7 @@ class Graph:
|
|
|
138
138
|
with self._lock:
|
|
139
139
|
con = self._connect()
|
|
140
140
|
store.install(con)
|
|
141
|
+
refusals.install(con)
|
|
141
142
|
|
|
142
143
|
# ── connections ─────────────────────────────────────────────────
|
|
143
144
|
def _connect(self) -> sqlite3.Connection:
|
|
@@ -569,8 +570,10 @@ class Graph:
|
|
|
569
570
|
if type == "claim" and author not in P.curators:
|
|
570
571
|
problems = claim_issues(s, body, self._support_ids(s, nid, pending), inv)
|
|
571
572
|
if problems:
|
|
572
|
-
|
|
573
|
-
|
|
573
|
+
found = [p.to_dict() for p in problems]
|
|
574
|
+
refusals.record_refusal(self, author=author, op="create", inv=inv, node="", body=body,
|
|
575
|
+
problems=found)
|
|
576
|
+
raise GraphError("claim refused:\n- " + "\n- ".join(p.text() for p in problems), found)
|
|
574
577
|
|
|
575
578
|
def write(con):
|
|
576
579
|
self._rate(con, author)
|
|
@@ -591,6 +594,11 @@ class Graph:
|
|
|
591
594
|
out = self.get(nid)
|
|
592
595
|
if flipped:
|
|
593
596
|
out["notes"] = [f"link turned round to the direction the relation allows: {x}" for x in flipped]
|
|
597
|
+
if type == "claim" and author not in P.curators:
|
|
598
|
+
fixed = refusals.match_fix(self, author=author, inv=inv, node=nid, statement=body.get("statement", ""),
|
|
599
|
+
op="create")
|
|
600
|
+
if fixed:
|
|
601
|
+
out["fixes_refusals"] = fixed
|
|
594
602
|
return out
|
|
595
603
|
|
|
596
604
|
def _may_revise(self, s: D.Snap, author: str, n: dict) -> bool:
|
|
@@ -627,8 +635,10 @@ class Graph:
|
|
|
627
635
|
if n["type"] == "claim" and author not in P.curators:
|
|
628
636
|
problems = claim_issues(s, body, self._support_ids(s, node, []), n["inv"])
|
|
629
637
|
if problems:
|
|
630
|
-
|
|
631
|
-
|
|
638
|
+
found = [p.to_dict() for p in problems]
|
|
639
|
+
refusals.record_refusal(self, author=author, op="revise", inv=n["inv"], node=node, body=body,
|
|
640
|
+
problems=found)
|
|
641
|
+
raise GraphError("revision refused:\n- " + "\n- ".join(p.text() for p in problems), found)
|
|
632
642
|
at = self.clock()
|
|
633
643
|
|
|
634
644
|
def write(con):
|
|
@@ -638,7 +648,13 @@ class Graph:
|
|
|
638
648
|
self._transaction(write)
|
|
639
649
|
finally:
|
|
640
650
|
self._after_write()
|
|
641
|
-
|
|
651
|
+
out = self.get(node)
|
|
652
|
+
if out["type"] == "claim" and author not in P.curators:
|
|
653
|
+
fixed = refusals.match_fix(self, author=author, inv=out["inv"], node=node,
|
|
654
|
+
statement=out["body"].get("statement", ""), op="revise")
|
|
655
|
+
if fixed:
|
|
656
|
+
out["fixes_refusals"] = fixed
|
|
657
|
+
return out
|
|
642
658
|
|
|
643
659
|
def link(self, *, author: str, src: str, rel: str, dst: str, why: str = "", data: dict | None = None) -> dict:
|
|
644
660
|
if rel == "reviews":
|
|
@@ -952,6 +968,25 @@ class Graph:
|
|
|
952
968
|
return {"seq": s.seq, "nodes": len(s.nodes), "edges": len(s.edges), "by_type": dict(by),
|
|
953
969
|
"leases": len(s.leases)}
|
|
954
970
|
|
|
971
|
+
# ── the refusal log (refusals.py) and checking write-ups (report_check.py) ──
|
|
972
|
+
def refusals(self, inv: str = "", author: str = "", since: str = "", limit: int = 500) -> list[dict]:
|
|
973
|
+
"""Every overclaim the record refused, newest first, with its codes and what fixed it (if anything)."""
|
|
974
|
+
return refusals.rows(self, inv=inv, author=author, since=since, limit=limit)
|
|
975
|
+
|
|
976
|
+
def refusal_report(self, inv: str = "", since: str = "") -> dict:
|
|
977
|
+
"""Overclaims caught per author and per rule, fixed vs open, with the latest in full (and as markdown)."""
|
|
978
|
+
return refusals.report(self, inv=inv, since=since)
|
|
979
|
+
|
|
980
|
+
def verify_refusals(self) -> dict:
|
|
981
|
+
"""The refusal log's own hash chain."""
|
|
982
|
+
return refusals.verify(self)
|
|
983
|
+
|
|
984
|
+
def check_text(self, text: str, inv: str = "") -> dict:
|
|
985
|
+
"""Lint a write-up (markdown) against the record: numbers no result contains, claims stated as settled that
|
|
986
|
+
the record has as contested / withdrawn / unchecked, codes that don't exist, absolutes nothing counts."""
|
|
987
|
+
from . import report_check
|
|
988
|
+
return report_check.check(self, text, inv)
|
|
989
|
+
|
|
955
990
|
# ── method & coordination (plan.py) ─────────────────────────────
|
|
956
991
|
def next_experiments(self, inv: str, limit: int = 10) -> dict:
|
|
957
992
|
from . import plan
|
|
@@ -24,7 +24,7 @@ COMPARATIVE = re.compile(
|
|
|
24
24
|
r"gains? over|uplift|lift over|advantage over|wins? over|surpass(?:es|ed)?)\b", re.I)
|
|
25
25
|
SIGNIFICANCE = re.compile(r"\b(significant(?:ly)?|statistically|reliably (?:better|worse|higher|lower))\b", re.I)
|
|
26
26
|
CAUSAL = re.compile(
|
|
27
|
-
r"\b(caus(?:es|ed|ing|al)|cause|leads? to|led to|
|
|
27
|
+
r"\b(caus(?:es|ed|ing|al)|cause|leads? to|led to|resulted in|drives?|driven by|due to|because of|"
|
|
28
28
|
r"responsible for|is the reason)\b", re.I)
|
|
29
29
|
EXCLUSION = re.compile(
|
|
30
30
|
r"\b(no (?:effect|difference|impact|influence|benefit|improvement|association|relationship)|"
|
|
@@ -10,7 +10,8 @@ no absolutes without k of n, exclusions need a powered search. A refusal comes b
|
|
|
10
10
|
fix, which is the point.
|
|
11
11
|
|
|
12
12
|
Tools: overview · find · show · start_investigation · add · link · revise · withdraw · retract_link · review ·
|
|
13
|
-
next_experiments · work_queue · claim_work · register_run · bound · report · verify ·
|
|
13
|
+
next_experiments · work_queue · claim_work · register_run · bound · check_report · refusal_log · report · verify ·
|
|
14
|
+
schema.
|
|
14
15
|
Resources: hypgraph://skill · hypgraph://schema · hypgraph://investigations · hypgraph://investigation/{inv}/report.
|
|
15
16
|
Prompt: method — how to work a question through the record.
|
|
16
17
|
"""
|
|
@@ -448,6 +449,32 @@ def build(graph: Graph | str, author: Optional[str] = None, files_root: str | No
|
|
|
448
449
|
raise ToolError("give k (how many times it happened) and n (trials), with 0 <= k <= n")
|
|
449
450
|
return bound_text(k, n, exposure, unit)
|
|
450
451
|
|
|
452
|
+
@mcp.tool
|
|
453
|
+
def check_report(text: str, investigation: str = "") -> str:
|
|
454
|
+
"""Lint a write-up (markdown) against the record BEFORE you hand it over: numbers no result contains, claims
|
|
455
|
+
stated as settled that the record has as unchecked / contested / withdrawn, codes that don't exist, never /
|
|
456
|
+
no effect / significantly with nothing behind them. Fix every error; say why for any warning you keep."""
|
|
457
|
+
try:
|
|
458
|
+
r = g.check_text(text, g.resolve(investigation) if investigation else "")
|
|
459
|
+
except (GraphError, KeyError) as e:
|
|
460
|
+
fail(e)
|
|
461
|
+
return r["markdown"]
|
|
462
|
+
|
|
463
|
+
@mcp.tool
|
|
464
|
+
def refusal_log(investigation: str = "", author: str = "") -> str:
|
|
465
|
+
"""The overclaims the record refused — per author and rule, fixed or still open. With author: that author's
|
|
466
|
+
refusals in full, so you can see what you (or a teammate) tried to say and what the record said back."""
|
|
467
|
+
try:
|
|
468
|
+
inv = g.resolve(investigation) if investigation else ""
|
|
469
|
+
if author:
|
|
470
|
+
rows = g.refusals(inv=inv, author=author.strip().lower())
|
|
471
|
+
return "\n".join(f"{r['at'][:16]} {', '.join(r['codes'])}: “{r['statement'][:200]}”"
|
|
472
|
+
+ (f" → fixed as {r['fixed_by']['node']}" if r["fixed_by"] else " → not fixed")
|
|
473
|
+
for r in rows[:40]) or "No refusals."
|
|
474
|
+
return g.refusal_report(inv=inv)["markdown"]
|
|
475
|
+
except (GraphError, KeyError) as e:
|
|
476
|
+
fail(e)
|
|
477
|
+
|
|
451
478
|
@mcp.tool
|
|
452
479
|
def schema() -> dict:
|
|
453
480
|
"""The node types (fields, which are required), the relations (which types each may join) and the verdicts."""
|