hypgraph 0.8.0__tar.gz → 0.8.2__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.8.0 → hypgraph-0.8.2}/CHANGELOG.md +42 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/PKG-INFO +29 -1
- {hypgraph-0.8.0 → hypgraph-0.8.2}/README.md +28 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/docs/PLAN.md +1 -1
- {hypgraph-0.8.0 → hypgraph-0.8.2}/docs/RELEASE-READINESS.md +22 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/docs/spec.md +12 -5
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/__init__.py +1 -1
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/assertions.py +77 -21
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/checks.py +59 -14
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/cli.py +17 -6
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/demos/__init__.py +1 -1
- hypgraph-0.8.2/src/hypgraph/demos/dispute.py +168 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_assertions.py +127 -8
- hypgraph-0.8.2/tests/test_demo_dispute.py +52 -0
- hypgraph-0.8.2/tests/test_quoted_counterexample.py +107 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/.gitignore +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/AGENTS.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/CITATION.cff +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/DISCLAIMER.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/LICENSE +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/NOTICE +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/docs/decisions/ADR-002-governance-profiles.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/docs/profile-workflows-plan.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/docs/review-2026-10-24.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/docs/site-plan.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/docs/ui-plan.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/docs/verification.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/docs/workflows/README.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/docs/workflows/knowledge.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/docs/workflows/optimization.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/docs/workflows/scientific.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/docs/workflows/software.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/docs/workflows/trading.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/examples/frameworks/README.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/pyproject.toml +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/__main__.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/blind.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/bundle.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/coord.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/demos/broad_street.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/demos/linked.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/derive.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/graph.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/language.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/mcp.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/plan.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/policy.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/profiles.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/references.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/refusals.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/report_check.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/schema.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/server.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/skill/SKILL.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/stats.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/store.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/ui/__init__.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/ui/atlas.css +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/ui/atlas.js +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/ui/hypgraph.css +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/ui/hypgraph.i18n.js +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/ui/hypgraph.js +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/ui/hypgraph.svg +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/ui/index.html +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/ui/vendor/cose-base.js +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/ui/vendor/layout-base.js +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/src/hypgraph/workflow.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_accept_work_kind.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_anchoring.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_blind.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_blind_oracles.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_borrowed_words.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_bound_support.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_bundle_telemetry.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_cancel_releases_lease.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_cited_and_failed_support.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_contradictions.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_coord.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_counts_that_bear.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_d10.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_direction_and_units.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_engine.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_engine_fixes.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_figure_said_twice.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_framework_examples.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_i18n.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_incremental.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_link_translate.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_linked_records.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_log_and_check.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_measure_questions.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_opposed_answers.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_option_lead.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_order_detail.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_profiles.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_profiles_041.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_quantifiers_and_identity.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_questions_not_assertions.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_references.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_refusal_friction.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_refusal_results.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_refusals.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_relation_words.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_report_answers.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_rerunnable.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_restates_scale.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_server_auth.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_server_hosts.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_shape_refusals.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_stats_not_backtests.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_stats_values.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_surfaces.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_telemetry_stream.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_trust_boundary.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_typed_audits.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_ui_app.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_ui_engine.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_uncorrected_effect.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_unnamed_inputs.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_verify_complete.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_verify_lifecycle.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_verify_scope.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_verify_scoped_views.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_verify_trust.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_verify_views.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_versions_not_figures.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_vocabulary_friction.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_what_a_count_is_of.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.2}/tests/test_workflows.py +0 -0
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.2 — 2026-11-01: an extreme that quotes its own counterexample; a disputed answer
|
|
4
|
+
|
|
5
|
+
- **A quoted counterexample no longer excuses an extreme.** A stated count used to exempt an extreme whenever its
|
|
6
|
+
clause said "k of n ... than", meant for a count about a figure of the statement's own ("the shortest is 21 km; 1 of
|
|
7
|
+
77 are shorter than 26 km"). It also exempted the count at the extreme's own figure, so "the shortest route is 30 km,
|
|
8
|
+
since 1 of 77 are shorter than 30 km" was accepted. The count is now an exception to the extreme whenever its figure,
|
|
9
|
+
on the result or in the sentence, is the extreme's own.
|
|
10
|
+
- **Two readings that exemption had hidden.** "The highest ... is therefore not 125" is a denial (an adverb between
|
|
11
|
+
the copula and "not"), and a bound in the extreme's own direction ("the shortest is at most 30 km", "91 or more") is
|
|
12
|
+
not a claim that the figure is the extreme. A bound the other way ("the shortest is at least 30 km") still claims it.
|
|
13
|
+
- Replayed on 1,900 recorded claim writes: 5 change, all now refused. Four are an extreme stated beside the count
|
|
14
|
+
that shows it false. One is a retraction worded as the claim ("the highest is 80, but this is not correct: 3 of
|
|
15
|
+
1,024 are worth more"). Who the record names as still saying something false is unchanged or better on every set.
|
|
16
|
+
- **`hypgraph demo dispute`: a disputed answer.** A teaching record in which two parties answer one question from the
|
|
17
|
+
same road register. Ada makes an honest mistake, the record refuses it (`COUNT_CONTRADICTS`) and she corrects it.
|
|
18
|
+
Mallory cites the register's units note for a claim it contradicts, is refused twice (`SOURCE_CONTRADICTS`) and files
|
|
19
|
+
it without the note. An independent reviewer checks one claim and fails the other, so the Evidence view shows one
|
|
20
|
+
answer checked and one contested, and `Graph.conduct()` names who kept saying it. It is also on the website
|
|
21
|
+
(`examples/dispute/`, in every site language) and in the README. `hypgraph demo` with no name still builds Broad
|
|
22
|
+
Street.
|
|
23
|
+
|
|
24
|
+
## 0.8.1 — 2026-11-01: exact figures, units within their kind
|
|
25
|
+
|
|
26
|
+
An independent audit of 0.8.0 found two defects in the typed checks, both confirmed on the published wheel.
|
|
27
|
+
|
|
28
|
+
- **Exact figures (HG-080-01).** Containment of two conditions was decided by probing at each figure ± 1 and the
|
|
29
|
+
midpoints, in floats. Above 2**53 a figure + 1 is the figure, so x > 1e18 looked inside x <= 1e18, and a count of 0
|
|
30
|
+
above 1e18 supported "none below 1e18". Containment is now decided on the pieces the two figures cut the line into,
|
|
31
|
+
with no arithmetic on them, and figures keep their exact value (10**18 + 1 is not 1e18; it renders as itself).
|
|
32
|
+
- **Units within their kind (HG-080-02).** The unit table's kind was dropped, so `lb` (mass) matched `GBP` and
|
|
33
|
+
`sterling` (currency) through the shared name "pound". A unit is now its (kind, unit) readings: `lb` = `lbs` and
|
|
34
|
+
`GBP` = `sterling` still, never across kinds, and an ambiguous `pound` matches only another `pound`; the refusal says
|
|
35
|
+
what it could be and asks for `lb` or `GBP`.
|
|
36
|
+
- **`exactly` decided from bounds.** It was left unresolved unless its set was the counted one or its complement:
|
|
37
|
+
4 of 4 above F now contradicts "exactly 4 below F". Over one population every verdict now equals enumeration of the
|
|
38
|
+
consistent worlds (sound and complete), and the test checks that.
|
|
39
|
+
- **An extreme stated as a value is two atoms.** In a typed investigation "the longest of the 4 routes is 26 km"
|
|
40
|
+
needs both "none above 26" and "some at 26 or more"; one of them is refused (`ASSERTION_MISSING`). An extreme stated
|
|
41
|
+
as a bound ("the slowest took at least 40 minutes") needs one.
|
|
42
|
+
- Tests: containment against exact rational probes over large, adjacent, subnormal and signed-zero figures; the audit's
|
|
43
|
+
cases through `_decide` and the write gate; units; extremes. The audit's own test file passes (13 of 13).
|
|
44
|
+
|
|
3
45
|
## 0.8.0 — 2026-11-01: typed claim assertions
|
|
4
46
|
|
|
5
47
|
Eight independent audits (HG-020 → HG-028) found the same class of failure, each time in a new wording: to decide
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hypgraph
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.2
|
|
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/
|
|
@@ -136,6 +136,34 @@ CL1 — withdrawn claim · …
|
|
|
136
136
|
⚠ 2 nodes rest on CL1: 1 decision (D1); 1 claim (CL2) — now AT RISK
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
+
## Calling out a bad answer
|
|
140
|
+
|
|
141
|
+
Two agents answer one question from the same road register. One makes an honest mistake and the other lies. The
|
|
142
|
+
record tells them apart, and nobody has to label anyone:
|
|
143
|
+
|
|
144
|
+
<p align="center"><a href="https://hypgraph.com/examples/dispute/"><picture>
|
|
145
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://hypgraph.com/assets/readme/dispute-dark.png">
|
|
146
|
+
<img src="https://hypgraph.com/assets/readme/dispute-light.png" width="880" alt="The Evidence view of a disputed answer: Ada's claim that the shortest route is 29 km is checked, Mallory's claim that the register is in miles is contested, above the two route counts the instrument filed and the seven register items they rest on">
|
|
147
|
+
</picture></a></p>
|
|
148
|
+
<p align="center"><sub>Standing derived from the record: one answer checked, the other contested. <code>hypgraph demo dispute</code> builds it; <a href="https://hypgraph.com/examples/dispute/">open it live</a>.</sub></p>
|
|
149
|
+
|
|
150
|
+
- **Ada** says the shortest route is 30 km. The instrument counted 1 of the 4 routes as shorter, so the record refuses
|
|
151
|
+
it (`COUNT_CONTRADICTS`). She corrects it to 29 km, which the count backs.
|
|
152
|
+
- **Mallory** says the register is in miles, and cites the register's own units note as support. The note says
|
|
153
|
+
kilometres, so the record refuses it (`SOURCE_CONTRADICTS`). She says it again and is refused again.
|
|
154
|
+
|
|
155
|
+
The record can't catch every lie by itself: once Mallory stops citing the note, her claim goes in. But it keeps what
|
|
156
|
+
she was shown. The reviewer fails her claim against the note, and the conduct log (`hypgraph refusals`,
|
|
157
|
+
`Graph.conduct()`) says who corrected what they were shown and who kept saying it:
|
|
158
|
+
|
|
159
|
+
```text
|
|
160
|
+
$ hypgraph demo dispute
|
|
161
|
+
…
|
|
162
|
+
What the record says of each author:
|
|
163
|
+
ada the record's own count contradicted a claim of this author's 1 time; the author did not say it again, and corrected it 1 time
|
|
164
|
+
mallory 1 claim of this author's went against a source it cited as support; the author said it again 2 times after being shown, and never corrected it; its last word still says it
|
|
165
|
+
```
|
|
166
|
+
|
|
139
167
|
## Derived, never asserted
|
|
140
168
|
|
|
141
169
|
Nothing in the record is ever *set* as known. How well each claim is known is **derived on every read**:
|
|
@@ -104,6 +104,34 @@ CL1 — withdrawn claim · …
|
|
|
104
104
|
⚠ 2 nodes rest on CL1: 1 decision (D1); 1 claim (CL2) — now AT RISK
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
+
## Calling out a bad answer
|
|
108
|
+
|
|
109
|
+
Two agents answer one question from the same road register. One makes an honest mistake and the other lies. The
|
|
110
|
+
record tells them apart, and nobody has to label anyone:
|
|
111
|
+
|
|
112
|
+
<p align="center"><a href="https://hypgraph.com/examples/dispute/"><picture>
|
|
113
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://hypgraph.com/assets/readme/dispute-dark.png">
|
|
114
|
+
<img src="https://hypgraph.com/assets/readme/dispute-light.png" width="880" alt="The Evidence view of a disputed answer: Ada's claim that the shortest route is 29 km is checked, Mallory's claim that the register is in miles is contested, above the two route counts the instrument filed and the seven register items they rest on">
|
|
115
|
+
</picture></a></p>
|
|
116
|
+
<p align="center"><sub>Standing derived from the record: one answer checked, the other contested. <code>hypgraph demo dispute</code> builds it; <a href="https://hypgraph.com/examples/dispute/">open it live</a>.</sub></p>
|
|
117
|
+
|
|
118
|
+
- **Ada** says the shortest route is 30 km. The instrument counted 1 of the 4 routes as shorter, so the record refuses
|
|
119
|
+
it (`COUNT_CONTRADICTS`). She corrects it to 29 km, which the count backs.
|
|
120
|
+
- **Mallory** says the register is in miles, and cites the register's own units note as support. The note says
|
|
121
|
+
kilometres, so the record refuses it (`SOURCE_CONTRADICTS`). She says it again and is refused again.
|
|
122
|
+
|
|
123
|
+
The record can't catch every lie by itself: once Mallory stops citing the note, her claim goes in. But it keeps what
|
|
124
|
+
she was shown. The reviewer fails her claim against the note, and the conduct log (`hypgraph refusals`,
|
|
125
|
+
`Graph.conduct()`) says who corrected what they were shown and who kept saying it:
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
$ hypgraph demo dispute
|
|
129
|
+
…
|
|
130
|
+
What the record says of each author:
|
|
131
|
+
ada the record's own count contradicted a claim of this author's 1 time; the author did not say it again, and corrected it 1 time
|
|
132
|
+
mallory 1 claim of this author's went against a source it cited as support; the author said it again 2 times after being shown, and never corrected it; its last word still says it
|
|
133
|
+
```
|
|
134
|
+
|
|
107
135
|
## Derived, never asserted
|
|
108
136
|
|
|
109
137
|
Nothing in the record is ever *set* as known. How well each claim is known is **derived on every read**:
|
|
@@ -109,7 +109,7 @@ claim's derived standing.
|
|
|
109
109
|
Multi-writer sync between graphs (bundles cover sharing), authentication (the host's job; the HTTP server binds to
|
|
110
110
|
loopback and is read-only unless started with `--writable`), and a hosted service.
|
|
111
111
|
|
|
112
|
-
## Status at 0.8.
|
|
112
|
+
## Status at 0.8.2 (2026-11-01)
|
|
113
113
|
|
|
114
114
|
All seven phases are done: the library, bundles, the server and UI, the MCP server, the website, Pegasus running on
|
|
115
115
|
the package, and releases to PyPI from tags with trusted publishing. What came after the plan, by release, is in
|
|
@@ -234,6 +234,28 @@ was taken from a changelog without being checked against the code.*
|
|
|
234
234
|
> verified, and the record shows the atoms, not the sentence, as what it checked. Replaying the 1,900 recorded
|
|
235
235
|
> adversarial entries changes none. #102 is merged with green CI (17 of 17 checks). Released as 0.8.0.
|
|
236
236
|
|
|
237
|
+
> **Audit of 0.8.0 (2026-11-01).** The first audit of the typed layer found its architecture sound (GO for a
|
|
238
|
+
> controlled typed-claims pilot) and two narrow defects in its helpers, both reproduced on the published wheel (its
|
|
239
|
+
> test file: 9 failed, the 4 controls passed; the wheel's SHA-256 matched the audit's). Containment was probed in
|
|
240
|
+
> floats at figure ± 1, which above 2**53 is the figure itself, so opposite conditions at 1e18 looked equal and an
|
|
241
|
+
> unsupported "none below 1e18" was supported (HG-080-01). And units dropped their kind, so `lb` matched `GBP` through
|
|
242
|
+
> the shared name "pound" (HG-080-02). Containment is now decided exactly on the pieces of the line, with figures kept
|
|
243
|
+
> as given; a unit is its (kind, unit) readings. Two related gaps closed with them: `exactly` was left open unless its
|
|
244
|
+
> set was the counted one (now decided from bounds, and every verdict equals enumeration over one population), and an
|
|
245
|
+
> extreme stated as a value now needs both its atoms.
|
|
246
|
+
>
|
|
247
|
+
> **Where that stands.** The boundary the audit names stays: prose is matched to atoms by reading words, so a missed
|
|
248
|
+
> part leaves prose unverified, never an atom verified; integrations should consume `asserts` and their verdicts, not
|
|
249
|
+
> the statement. Untyped mode keeps its documented limits. #104 is merged with green CI (17 of 17 checks). Released as
|
|
250
|
+
> 0.8.1.
|
|
251
|
+
|
|
252
|
+
> **After 0.8.1 (2026-11-01).** The frozen word checks were changed once, for a soundness gap rather than a new
|
|
253
|
+
> wording: a stated count exempted an extreme even when the count was at the extreme's own figure, so an extreme that
|
|
254
|
+
> quoted its own counterexample was accepted. The exemption now holds only for a count about another figure; an adverb
|
|
255
|
+
> before "not" and a bound in the extreme's own direction are read as what they are. Replaying the 1,900 recorded
|
|
256
|
+
> claim writes changes 5, all now refused (4 lies, 1 retraction worded as the claim). #106 is merged with green CI.
|
|
257
|
+
> Released as 0.8.2, with the `dispute` demo (#107).
|
|
258
|
+
|
|
237
259
|
## Recommendation: CONDITIONAL GO
|
|
238
260
|
|
|
239
261
|
HypGraph is ready for a public developer preview once #59, #60 and #61 are merged with green CI and released as one
|
|
@@ -290,22 +290,29 @@ smallest detectable difference uses 80% power at two-sided α = 0.05.
|
|
|
290
290
|
|
|
291
291
|
**Typed claims.** An atom (§2) is decided by its bound count alone; no words are read. Let S_e = {x : x op_e f_e} (what
|
|
292
292
|
was counted, k of n inside it, n − k outside) and S_a = {x : x op_a f_a} (what is asserted); P_e and P_a the
|
|
293
|
-
populations. The property must equal the count's and the unit match it
|
|
294
|
-
|
|
293
|
+
populations. The property must equal the count's and the unit match it: spellings of one unit are one unit within one
|
|
294
|
+
kind of unit (`lb` = `lbs`, `GBP` = `sterling`, never `lb` = `GBP`), a spelling that could be two kinds (`pound`:
|
|
295
|
+
mass or currency) matches only another that could be the same two, and nothing is converted. A stated `n` must equal
|
|
296
|
+
the count's. Figures are compared exactly, as given (an int or a float, at any magnitude), on the pieces of the line
|
|
297
|
+
the two figures cut it into — never by arithmetic on them.
|
|
295
298
|
|
|
296
299
|
- `none` S_a: **supported** when a side of the count with 0 inside it contains S_a and P_a lies within P_e;
|
|
297
300
|
**contradicted** when a side with more than 0 lies inside S_a and P_e lies within P_a.
|
|
298
301
|
- `some` S_a: supported when a side with more than 0 lies inside S_a and P_e within P_a; contradicted when a side with
|
|
299
302
|
0 contains S_a and P_a lies within P_e.
|
|
300
|
-
- `all` S_a is `none` of the complement of S_a.
|
|
301
|
-
|
|
303
|
+
- `all` S_a is `none` of the complement of S_a.
|
|
304
|
+
- `exactly` k needs the same population. A side of the count lying inside S_a puts at least its count in S_a; a side
|
|
305
|
+
containing S_a, at most its count. k at both bounds is **supported**, k outside them **contradicted**.
|
|
306
|
+
|
|
307
|
+
Over one population these verdicts are complete as well as sound: supported or contradicted exactly when every set of
|
|
308
|
+
values consistent with the count agrees.
|
|
302
309
|
- Anything else is **unresolved**, and names the field that blocked it.
|
|
303
310
|
|
|
304
311
|
| Code | Refused when |
|
|
305
312
|
|---|---|
|
|
306
313
|
| `ASSERTION_CONTRADICTED` | an atom is false by its count. The refusal log tracks it like a count contradiction (§8). |
|
|
307
314
|
| `ASSERTION_UNRESOLVED` | an atom its count can't settle: another property or unit, a population neither within nor containing the count's, a condition the count's doesn't decide, an untyped count, a malformed atom. |
|
|
308
|
-
| `ASSERTION_MISSING` | in a `typed_claims` investigation, the statement
|
|
315
|
+
| `ASSERTION_MISSING` | in a `typed_claims` investigation, the statement's parts that assert an absolute, an extreme or a count need more atoms than the claim has: one each, and two for an extreme stated as a value ("the longest is 26 km": none above, some at or above). Detection reads words only to ask for atoms; a missed word leaves prose unverified, never an atom verified. The refusal proposes atoms drafted from the statement and the claim's typed counts, which are checked like any other once written. |
|
|
309
316
|
|
|
310
317
|
Atoms are checked in every investigation that has them. In a `typed_claims` investigation the word interpretation
|
|
311
318
|
stands down: a measurement's name and key are not read for what was counted, and rule 5 (absolutes, with the
|
|
@@ -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.8.
|
|
19
|
+
__version__ = "0.8.2"
|
|
20
20
|
|
|
21
21
|
from .bundle import anchored, verify_bundle # noqa: E402
|
|
22
22
|
from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
|
|
@@ -18,7 +18,9 @@ steered one of those inferences. Here the instrument types what it counted and t
|
|
|
18
18
|
population a path: ``route/passenger`` lies within ``route``, so a count of 0 over every route also settles
|
|
19
19
|
"no passenger route"; a count of the passenger routes says nothing of the cargo routes.
|
|
20
20
|
property an id, compared for equality ("length" is not "speed").
|
|
21
|
-
unit compared for equality, spellings of one unit read as one ("km" = "kilometres")
|
|
21
|
+
unit compared for equality, spellings of one unit read as one ("km" = "kilometres") within one kind of unit:
|
|
22
|
+
"lb" is mass and "GBP" currency, never the same; "pound", which could be either, matches only
|
|
23
|
+
another spelling that could be either. Never converted.
|
|
22
24
|
n optional: the population's size as the claim states it; must be the count's n.
|
|
23
25
|
|
|
24
26
|
Each atom is decided on its own, by sets on the number line — S_e = {x : x op_e figure_e} (what was counted) and
|
|
@@ -28,7 +30,13 @@ S_a = {x : x op_a figure_a} (what is asserted) — and the paths. A count of k i
|
|
|
28
30
|
contradicted when a count above 0 is over a set inside S_a and a population inside the atom's
|
|
29
31
|
some S_a the mirror: a count above 0 inside S_a supports it, a count of 0 containing S_a contradicts it
|
|
30
32
|
all S_a none of the complement of S_a
|
|
31
|
-
exactly the same set
|
|
33
|
+
exactly the same population; a count over a set inside S_a puts at least that many in it, a count over a
|
|
34
|
+
set containing S_a at most that many: k inside those bounds, at both ends supported, outside them
|
|
35
|
+
contradicted (4 of 4 above F puts at most 0 below it, so "exactly 4 below F" is false)
|
|
36
|
+
|
|
37
|
+
Figures are compared exactly, at any magnitude: containment is decided on the pieces of the number line the two
|
|
38
|
+
figures cut it into (the figures themselves and the open intervals between), with no arithmetic on the figures — a
|
|
39
|
+
probe at figure + 1 is the figure itself above 2**53, which made x > 1e18 look inside x <= 1e18 (HG-080-01).
|
|
32
40
|
|
|
33
41
|
Anything else is unresolved, and the verdict names the field that blocked it. One count can back two atoms only if
|
|
34
42
|
each checks out against it on its own, so a true absolute can't lend anything to a false one (HG-028-01), and "all"
|
|
@@ -76,29 +84,56 @@ def prop(p) -> str | None:
|
|
|
76
84
|
|
|
77
85
|
|
|
78
86
|
def _unit(u) -> frozenset | None:
|
|
79
|
-
"""A unit as the
|
|
87
|
+
"""A unit as the (kind, unit) pairs it could be ("km" → {(length, kilometre)}, "pound" → {(mass, pound),
|
|
88
|
+
(currency, pound)}); an unknown spelling is itself. None when there's none."""
|
|
80
89
|
if u is None or (isinstance(u, str) and not u.strip()):
|
|
81
90
|
return None
|
|
82
91
|
from .checks import _UNIT_OF # one table of unit spellings, shared with the word checks
|
|
83
92
|
w = str(u).strip().lower()
|
|
84
|
-
return frozenset(
|
|
93
|
+
return frozenset(_UNIT_OF.get(w, ())) or frozenset({("", w)})
|
|
85
94
|
|
|
86
95
|
|
|
87
96
|
def same_unit(a, b) -> bool:
|
|
88
|
-
|
|
89
|
-
|
|
97
|
+
"""One unit: every reading of one is a reading of the other. "lb" = "lbs" and "GBP" = "sterling", but "lb" is
|
|
98
|
+
never "GBP" (HG-080-02), and an ambiguous "pound" is neither of them."""
|
|
99
|
+
return _unit(a) == _unit(b)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def ambiguous(u) -> list[str]:
|
|
103
|
+
"""The kinds of unit an ambiguous spelling could be ("pound": currency, mass); [] when it is one unit."""
|
|
104
|
+
kinds = sorted({k for k, _c in _unit(u) or ()})
|
|
105
|
+
return kinds if len(kinds) > 1 else []
|
|
90
106
|
|
|
91
107
|
|
|
92
108
|
def holds(x: float, op: str, f: float) -> bool:
|
|
93
109
|
return {"<": x < f, "<=": x <= f, "=": x == f, "!=": x != f, ">=": x >= f, ">": x > f}[op]
|
|
94
110
|
|
|
95
111
|
|
|
112
|
+
def _pieces(*figures) -> list[tuple]:
|
|
113
|
+
"""The number line cut at the figures: each figure as a point ("pt", x) and the open intervals between and beyond
|
|
114
|
+
them ("iv", lo, hi), None for an infinite end. Figures are compared as given (Python compares ints and floats
|
|
115
|
+
exactly); nothing is added to or averaged from them."""
|
|
116
|
+
pts = sorted(set(figures))
|
|
117
|
+
out: list[tuple] = [("iv", None, pts[0])]
|
|
118
|
+
for x, y in zip(pts, pts[1:]):
|
|
119
|
+
out += [("pt", x), ("iv", x, y)]
|
|
120
|
+
return out + [("pt", pts[-1]), ("iv", pts[-1], None)]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _inside(piece: tuple, op: str, f) -> bool:
|
|
124
|
+
"""Is a piece of the line inside {x : x op f}? f is one of the cuts, so an open interval lies wholly below or
|
|
125
|
+
wholly above it, and which side decides."""
|
|
126
|
+
if piece[0] == "pt":
|
|
127
|
+
return holds(piece[1], op, f)
|
|
128
|
+
_iv, _lo, hi = piece
|
|
129
|
+
below = hi is not None and hi <= f
|
|
130
|
+
return op in ("<", "<=", "!=") if below else op in (">", ">=", "!=")
|
|
131
|
+
|
|
132
|
+
|
|
96
133
|
def subset(a: tuple, b: tuple) -> bool:
|
|
97
|
-
"""Is {x : x a[0] a[1]} inside {x : x b[0] b[1]}? Both sets are unions of
|
|
98
|
-
so
|
|
99
|
-
|
|
100
|
-
probes = pts + [pts[0] - 1, pts[-1] + 1] + [(x + y) / 2 for x, y in zip(pts, pts[1:])]
|
|
101
|
-
return all(holds(x, *b) for x in probes if holds(x, *a))
|
|
134
|
+
"""Is {x : x a[0] a[1]} inside {x : x b[0] b[1]}? Both sets are unions of the pieces the two figures cut the line
|
|
135
|
+
into, so it is decided piece by piece, exactly, at any magnitude."""
|
|
136
|
+
return all(_inside(p, *b) for p in _pieces(a[1], b[1]) if _inside(p, *a))
|
|
102
137
|
|
|
103
138
|
|
|
104
139
|
def same_set(a: tuple, b: tuple) -> bool:
|
|
@@ -122,7 +157,7 @@ def count_fields(rv) -> tuple[dict | None, str]:
|
|
|
122
157
|
pop = path(rv["population"])
|
|
123
158
|
if pop is None:
|
|
124
159
|
return None, "the count's population is not a path (route/passenger)"
|
|
125
|
-
return {"k": int(k), "n": int(n), "op": rv["op"], "figure":
|
|
160
|
+
return {"k": int(k), "n": int(n), "op": rv["op"], "figure": rv["figure"], "population": pop,
|
|
126
161
|
"property": prop(rv["property"]), "unit": rv.get("unit")}, ""
|
|
127
162
|
|
|
128
163
|
|
|
@@ -171,9 +206,18 @@ def _cond(a: dict) -> str:
|
|
|
171
206
|
return f"{a['property']} {SHOW_OP.get(a['op'], a['op'])} {fig}{u}"
|
|
172
207
|
|
|
173
208
|
|
|
209
|
+
def whole_form(f):
|
|
210
|
+
"""A figure with its exact value, an integral float as an int (26.0 → 26, 1e18 → 10**18): 26 and 26.0 are one
|
|
211
|
+
figure. (Not ``f.is_integer()`` on an int — that is Python 3.12+.)"""
|
|
212
|
+
return f if isinstance(f, int) else int(f) if float(f).is_integer() else f
|
|
213
|
+
|
|
214
|
+
|
|
174
215
|
def stats_fmt(v) -> str:
|
|
216
|
+
"""A figure as it is: ints whole, floats exact (repr round-trips), so a rendered figure never hides a difference."""
|
|
217
|
+
if isinstance(v, int):
|
|
218
|
+
return str(v)
|
|
175
219
|
v = float(v)
|
|
176
|
-
return str(int(v)) if v.is_integer() else
|
|
220
|
+
return str(int(v)) if v.is_integer() and abs(v) < 1e15 else repr(v)
|
|
177
221
|
|
|
178
222
|
|
|
179
223
|
def render(a: dict) -> str:
|
|
@@ -213,8 +257,11 @@ def _decide(a: dict, e: dict) -> tuple[str, str, str]:
|
|
|
213
257
|
return "unresolved", "property", (f"the count is of {e['property']}, the assertion is about "
|
|
214
258
|
f"{prop(a['property'])}")
|
|
215
259
|
if not same_unit(a.get("unit"), e.get("unit")):
|
|
260
|
+
amb = next(((u, ambiguous(u)) for u in (a.get("unit"), e.get("unit")) if ambiguous(u)), None)
|
|
216
261
|
return "unresolved", "unit", (f"the count is in {e.get('unit') or 'no unit'}, the assertion in "
|
|
217
|
-
f"{a.get('unit') or 'no unit'}: state the figure in the count's unit"
|
|
262
|
+
f"{a.get('unit') or 'no unit'}: state the figure in the count's unit"
|
|
263
|
+
+ (f"; '{amb[0]}' could be {' or '.join(amb[1])}, so name the unit "
|
|
264
|
+
"unambiguously (lb, GBP)" if amb else ""))
|
|
218
265
|
if not (within(pa, pe) or within(pe, pa)):
|
|
219
266
|
return "unresolved", "population", (f"the count is of {_pop(pe)}, the assertion is about {_pop(pa)}: "
|
|
220
267
|
"neither lies within the other")
|
|
@@ -225,7 +272,7 @@ def _decide(a: dict, e: dict) -> tuple[str, str, str]:
|
|
|
225
272
|
f"{_pop(pe)} ({e['n']}) doesn't give")
|
|
226
273
|
# what was counted, both ways: k inside S_e and n - k outside it
|
|
227
274
|
sides = [((e["op"], e["figure"]), e["k"]), ((COMPLEMENT[e["op"]], e["figure"]), e["n"] - e["k"])]
|
|
228
|
-
q, t = a["quantifier"], (a["op"],
|
|
275
|
+
q, t = a["quantifier"], (a["op"], a["figure"])
|
|
229
276
|
got = _settle(q, t, sides, pa, pe, a)
|
|
230
277
|
if got:
|
|
231
278
|
return got
|
|
@@ -242,11 +289,18 @@ def _settle(q: str, t: tuple, sides: list, pa: tuple, pe: tuple, a: dict) -> tup
|
|
|
242
289
|
if q == "exactly":
|
|
243
290
|
if pa != pe:
|
|
244
291
|
return None
|
|
292
|
+
lo, hi = 0, sum(c for _side, c in sides)
|
|
245
293
|
for side, c in sides:
|
|
246
|
-
if
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
294
|
+
if subset(side, t): # everything counted there is in the atom's set
|
|
295
|
+
lo = max(lo, c)
|
|
296
|
+
if subset(t, side): # the atom's set lies inside what was counted there
|
|
297
|
+
hi = min(hi, c)
|
|
298
|
+
want = int(a["k"])
|
|
299
|
+
found = f"the count finds {lo}" if lo == hi else f"the count puts {lo} to {hi} there"
|
|
300
|
+
if lo == hi == want:
|
|
301
|
+
return "supported", "", found
|
|
302
|
+
if want < lo or want > hi:
|
|
303
|
+
return "contradicted", "k", f"{found}, not {want}"
|
|
250
304
|
return None
|
|
251
305
|
neg = "that don't" if q == "all" else "that do"
|
|
252
306
|
if q == "all": # everything meets it: nothing meets its complement
|
|
@@ -308,8 +362,10 @@ def key(a) -> str:
|
|
|
308
362
|
if atom_shape(a, None)[0]:
|
|
309
363
|
return json.dumps(a, sort_keys=True, default=str)[:300]
|
|
310
364
|
u = _unit(a.get("unit"))
|
|
365
|
+
f = a["figure"]
|
|
366
|
+
fig = whole_form(f) # 26 and 26.0 are one figure, exactly
|
|
311
367
|
return json.dumps([a["quantifier"], "/".join(path(a["population"]) or ()), prop(a["property"]), a["op"],
|
|
312
|
-
|
|
368
|
+
fig, sorted(u) if u else None, a.get("n"), a.get("k")], default=str)
|
|
313
369
|
|
|
314
370
|
|
|
315
371
|
def numbers(body: dict) -> list[float]:
|
|
@@ -375,7 +431,7 @@ def draft(s: Snap, body: dict, inv: str = "") -> list[dict]:
|
|
|
375
431
|
pool = [(i, e) for i, e in typed if e["n"] == n]
|
|
376
432
|
for i, e in [x for x in pool if x[1]["figure"] in figs] or pool:
|
|
377
433
|
add({"quantifier": "exactly", "population": _pop(e["population"]), "property": e["property"],
|
|
378
|
-
"op": e["op"], "figure":
|
|
434
|
+
"op": e["op"], "figure": whole_form(e["figure"]),
|
|
379
435
|
"k": k, "n": n, "measurement": i}, e)
|
|
380
436
|
return out[:6]
|
|
381
437
|
|
|
@@ -955,22 +955,34 @@ def claimed_extremes(stmt: str) -> list[str]:
|
|
|
955
955
|
if not _enumerated(stmt, w) and not _names_stated_count(stmt, w)]
|
|
956
956
|
|
|
957
957
|
|
|
958
|
+
# an adverb between the copula and its negation: "the highest ... is therefore not 125"
|
|
959
|
+
_ADVERB = r"(?:(?:therefore|thus|hence|then|so|also|still|\w+ly)\s+)?"
|
|
958
960
|
# the extreme's own predicate denied after it, in its clause: "the highest total value ... is not 130"
|
|
959
|
-
_DENIED_EXTREME = re.compile(r"\b(?:is|are|was|were)\s+(?:not|no longer|never)\b
|
|
961
|
+
_DENIED_EXTREME = re.compile(r"\b(?:is|are|was|were)\s+" + _ADVERB + r"(?:not|no longer|never)\b|"
|
|
962
|
+
r"\b(?:isn't|aren't|wasn't|weren't)\b|"
|
|
960
963
|
# or said to lie beyond a figure: "the true maximum is higher than 98"
|
|
961
964
|
r"\b(?:is|are|was|were|lies)\s+(?:\w+\s+)?(?:higher|lower|greater|larger|smaller|more|less|"
|
|
962
965
|
r"above|below|over|under|shorter|longer|beyond)\b", re.I)
|
|
963
966
|
# or denied before it: "120 is not the highest total value", "isn't the shortest"
|
|
964
|
-
_DENIED_BEFORE = re.compile(r"(?:\b(?:is|are|was|were)\s+(?:not|no longer|never)
|
|
967
|
+
_DENIED_BEFORE = re.compile(r"(?:\b(?:is|are|was|were)\s+" + _ADVERB + r"(?:not|no longer|never)|"
|
|
968
|
+
r"\b(?:isn't|aren't|wasn't|weren't))"
|
|
965
969
|
r"\s+(?:the\s+|a\s+|an\s+)?(?:true\s+|real\s+|actual\s+)?$", re.I)
|
|
966
970
|
_COPULA = re.compile(r"\b(?:is|are|was|were|isn't|aren't|wasn't|weren't|lies)\b", re.I)
|
|
967
971
|
|
|
968
972
|
|
|
969
|
-
def _denied(after: str) -> bool:
|
|
973
|
+
def _denied(after: str, w: str = "") -> bool:
|
|
970
974
|
"""Does the extreme's own predicate — the first copula after it — deny it or put it beyond a figure? Only that
|
|
971
|
-
one: "the highest value is 98 ..., so the true maximum is higher" still claims 98.
|
|
975
|
+
one: "the highest value is 98 ..., so the true maximum is higher" still claims 98. A bound in the extreme's own
|
|
976
|
+
direction is beyond its figure too: "the highest value is at least 91" (or "91 or more") says the true maximum is
|
|
977
|
+
91 or above, which what lies beyond 91 doesn't contradict; "the highest is at most 91" still claims a figure."""
|
|
972
978
|
cop = _COPULA.search(after)
|
|
973
|
-
|
|
979
|
+
if not cop:
|
|
980
|
+
return False
|
|
981
|
+
if _DENIED_EXTREME.match(after, cop.start()):
|
|
982
|
+
return True
|
|
983
|
+
own = {s for _a, s, _strict in _comparisons_in(w)}
|
|
984
|
+
pred = after[cop.end():].split(",")[0]
|
|
985
|
+
return bool(own) and any(not strict and s in own for _a, s, strict in _comparisons_in(pred))
|
|
974
986
|
|
|
975
987
|
|
|
976
988
|
# a count named for what lies beyond a figure ("fitting selections worth more", "shorter routes connecting") is about
|
|
@@ -988,7 +1000,7 @@ def _extremes(stmt: str) -> list[str]:
|
|
|
988
1000
|
for m in re.finditer(r"\b" + re.escape(w) + r"\b", stmt, re.I):
|
|
989
1001
|
end = next((x.start() for x in _CLAUSE.finditer(stmt, m.end())), len(stmt))
|
|
990
1002
|
start = max((x.end() for x in _CLAUSE.finditer(stmt, 0, m.start())), default=0)
|
|
991
|
-
if (not _denied(stmt[m.end():end]) and not _DENIED_BEFORE.search(stmt[start:m.start()])
|
|
1003
|
+
if (not _denied(stmt[m.end():end], w) and not _DENIED_BEFORE.search(stmt[start:m.start()])
|
|
992
1004
|
and re.search(r"\d", stmt[start:end])):
|
|
993
1005
|
out.append(w)
|
|
994
1006
|
break
|
|
@@ -1011,11 +1023,23 @@ def _count_contradicts(stmt: str, w: str, words: list, m: dict, k: int, n: int)
|
|
|
1011
1023
|
words=words + [f"={_fmt(v)}" for v in _subject_numbers(stmt, k, n)])
|
|
1012
1024
|
|
|
1013
1025
|
|
|
1014
|
-
|
|
1026
|
+
_THAN_FIGURE = re.compile(r"\bthan\s+\$?(-?\d[\d,]*(?:\.\d+)?)", re.I)
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
def _qualified(stmt: str, k: int, n: int, figs: frozenset = frozenset(), rv: dict | None = None) -> bool:
|
|
1015
1030
|
"""Does the statement give this count about a figure of its own ("1 of 61 routes are shorter than 26 km")?
|
|
1016
|
-
Then it's a count of something beyond that figure, not of exceptions to the extreme it claims.
|
|
1031
|
+
Then it's a count of something beyond that figure, not of exceptions to the extreme it claims. Not when the
|
|
1032
|
+
figure is the extreme's own: "the shortest route is 30 km, since 1 of 77 are shorter than 30 km" states the very
|
|
1033
|
+
count that shows it false, and quoting the counterexample doesn't make an extreme true."""
|
|
1034
|
+
if figs and rv is not None and stated_figure(rv) in figs:
|
|
1035
|
+
return False
|
|
1017
1036
|
forms = {f"{k} of {n}", f"{k:,} of {n:,}"}
|
|
1018
|
-
|
|
1037
|
+
for c in _CLAUSE.split(stmt):
|
|
1038
|
+
if any(f in c for f in forms) and re.search(r"\bthan\b", c):
|
|
1039
|
+
said = {float(x.replace(",", "")) for x in _THAN_FIGURE.findall(c)}
|
|
1040
|
+
if not (said & set(figs)):
|
|
1041
|
+
return True
|
|
1042
|
+
return False
|
|
1019
1043
|
|
|
1020
1044
|
|
|
1021
1045
|
def _superlative_contradicted(stmt: str, bound: list) -> list[Problem]:
|
|
@@ -1032,7 +1056,7 @@ def _superlative_contradicted(stmt: str, bound: list) -> list[Problem]:
|
|
|
1032
1056
|
and (L.content_words(f"{m.get('name', '')} {m.get('key', '')}") & about
|
|
1033
1057
|
or _BEYOND.search(f"{m.get('name', '')} {m.get('key', '')}".replace("_", " "))
|
|
1034
1058
|
or stated_figure(rv) in figs)
|
|
1035
|
-
and not (role == "refute" and _qualified(stmt, *a))]
|
|
1059
|
+
and not (role == "refute" and _qualified(stmt, *a, figs, rv))]
|
|
1036
1060
|
# a count beyond the extreme's own figure that finds some refutes it, whatever else is bound beside it
|
|
1037
1061
|
pinned = [(m, a) for m, rv, a, role in rel if role == "refute" and _pinned(m, rv, set(figs))]
|
|
1038
1062
|
if pinned:
|
|
@@ -1061,7 +1085,7 @@ def _extreme_claims(stmt: str) -> list[tuple[str, frozenset]]:
|
|
|
1061
1085
|
end = next((x.start() for x in _CLAUSE.finditer(stmt, m.end())), len(stmt))
|
|
1062
1086
|
start = max((x.end() for x in _CLAUSE.finditer(stmt, 0, m.start())), default=0)
|
|
1063
1087
|
after = stmt[m.end():end]
|
|
1064
|
-
if _denied(after) or _DENIED_BEFORE.search(stmt[start:m.start()]):
|
|
1088
|
+
if _denied(after, w) or _DENIED_BEFORE.search(stmt[start:m.start()]):
|
|
1065
1089
|
continue
|
|
1066
1090
|
cop = _COPULA.search(after)
|
|
1067
1091
|
fig = _FIGURE.match(after, cop.end()) if cop else None
|
|
@@ -1841,11 +1865,28 @@ def asserting_parts(stmt: str) -> list[str]:
|
|
|
1841
1865
|
if L.interrogative(p):
|
|
1842
1866
|
continue
|
|
1843
1867
|
quantified = any(_clause_comparisons(m.group("rest"), after_no=True) for m in _QUANT_CMP.finditer(p))
|
|
1844
|
-
if L.asserted(L.ABSOLUTE, p) or _extremes(p) or _STATED_COUNT.search(p) or quantified or
|
|
1868
|
+
if L.asserted(L.ABSOLUTE, p) or _extremes(p) or _STATED_COUNT.search(p) or quantified or _NO_NOUN_VERB.search(p):
|
|
1845
1869
|
out.append(p.strip())
|
|
1846
1870
|
return out
|
|
1847
1871
|
|
|
1848
1872
|
|
|
1873
|
+
_STATED_FIGURE = re.compile(r"(?<![\w.])\d[\d,]*(?:\.\d+)?")
|
|
1874
|
+
_POPULATION_SIZE = re.compile(r"\b(?:of|among|across|between|from|in|all)\s+(?:the\s+|all\s+|its\s+|these\s+|those\s+)?"
|
|
1875
|
+
r"(\d[\d,]*)\b(?!\s*(?:%|percent|per\s*cent))", re.I)
|
|
1876
|
+
|
|
1877
|
+
|
|
1878
|
+
def atoms_needed(part: str) -> int:
|
|
1879
|
+
"""How many atoms a part asks for: an extreme stated as a value ("the longest of the 4 routes is 26 km") is two —
|
|
1880
|
+
nothing above 26 and something at 26 or more; a figure it states only as a bound ("the slowest took at least 40
|
|
1881
|
+
minutes") is one. Over-counting asks for one more atom; it never marks one verified."""
|
|
1882
|
+
if not _extremes(part):
|
|
1883
|
+
return 1
|
|
1884
|
+
bounds = {f for f, _c in _clause_comparisons(part)}
|
|
1885
|
+
sizes = {m.start(1) for m in _POPULATION_SIZE.finditer(part)} # "of the 4 routes" is who, not a figure
|
|
1886
|
+
stated = {float(m.group(0).replace(",", "")) for m in _STATED_FIGURE.finditer(part) if m.start() not in sizes}
|
|
1887
|
+
return 2 if stated - bounds else 1
|
|
1888
|
+
|
|
1889
|
+
|
|
1849
1890
|
def assertion_issues(s: Snap, body: dict, inv: str, stmt: str, typed: bool) -> list[Problem]:
|
|
1850
1891
|
"""The claim's atoms checked against their counts, and — in a typed investigation — whether the statement asserts
|
|
1851
1892
|
more than its atoms do."""
|
|
@@ -1866,13 +1907,17 @@ def assertion_issues(s: Snap, body: dict, inv: str, stmt: str, typed: bool) -> l
|
|
|
1866
1907
|
f"{v['field']}", words=[v["field"]]))
|
|
1867
1908
|
if typed:
|
|
1868
1909
|
parts = asserting_parts(stmt)
|
|
1869
|
-
|
|
1910
|
+
needed = sum(atoms_needed(p) for p in parts)
|
|
1911
|
+
if needed > len(atoms):
|
|
1870
1912
|
proposed = A.draft(s, body, inv) if not atoms else []
|
|
1871
1913
|
hint = (" Proposed from the statement and its typed counts — check each before using it: "
|
|
1872
1914
|
+ json.dumps(proposed, separators=(",", ":"))[:900]) if proposed else ""
|
|
1873
1915
|
out.append(Problem("ASSERTION_MISSING", f"this investigation checks claims as typed assertions: the "
|
|
1874
1916
|
f"statement asserts {len(parts)} thing{'s' * (len(parts) != 1)} — "
|
|
1875
|
-
+ "; ".join(f"'{p[:80]}'" for p in parts[:4]) + f" —
|
|
1917
|
+
+ "; ".join(f"'{p[:80]}'" for p in parts[:4]) + f" — {needed} atom"
|
|
1918
|
+
+ "s" * (needed != 1) + " in all" + (" (an extreme stated as a value is two: none "
|
|
1919
|
+
"above it, some at it or beyond)" if needed > len(parts) else "")
|
|
1920
|
+
+ f", and the claim gives {len(atoms)} "
|
|
1876
1921
|
"atom" + "s" * (len(atoms) != 1) + ". Add an atom for each to asserts: {quantifier "
|
|
1877
1922
|
"(none | all | some | exactly), population, property, op, figure, unit, n, "
|
|
1878
1923
|
"measurement}." + hint, fix="add an atom to asserts for each thing the statement "
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
hypgraph — the command line.
|
|
3
3
|
|
|
4
4
|
hypgraph demo build the Broad Street (1854) teaching investigation into ./research.hypgraph
|
|
5
|
+
hypgraph demo dispute an honest mistake and a lie: the record checks one and contests the other
|
|
5
6
|
hypgraph serve open the HypGraph UI on http://127.0.0.1:8765
|
|
6
7
|
hypgraph init "Plant growth" --objective "…" --reviewers ada,lin
|
|
7
8
|
hypgraph add hypothesis statement="Blue light grows cress faster." predictions='["taller after 7 days"]'
|
|
@@ -351,19 +352,27 @@ def cmd_import(args) -> int:
|
|
|
351
352
|
|
|
352
353
|
|
|
353
354
|
def cmd_demo(args) -> int:
|
|
354
|
-
|
|
355
|
+
import importlib
|
|
356
|
+
|
|
357
|
+
from .demos import DEMOS
|
|
358
|
+
demo = importlib.import_module(DEMOS[args.name])
|
|
355
359
|
if Path(args.db).exists():
|
|
356
|
-
raise GraphError(f"{args.db} exists — choose another file: hypgraph demo --db
|
|
357
|
-
g, inv, story =
|
|
360
|
+
raise GraphError(f"{args.db} exists — choose another file: hypgraph demo {args.name} --db {args.name}.hypgraph")
|
|
361
|
+
g, inv, story = demo.build(path=args.db)
|
|
362
|
+
conduct = g.conduct(inv) if args.name == "dispute" else {}
|
|
358
363
|
if args.json:
|
|
359
|
-
_out(args, {"db": g.path, "inv": inv, "story": story})
|
|
364
|
+
_out(args, {"db": g.path, "inv": inv, "story": story, **({"conduct": conduct} if conduct else {})})
|
|
360
365
|
return 0
|
|
361
366
|
for s in story:
|
|
362
367
|
print(f"{s['at']} {s['actor']:<10} {s['text']}")
|
|
363
368
|
if "refused" in s:
|
|
364
369
|
print(" ✗ the record refused it:\n " +
|
|
365
370
|
s["refused"].replace("\n", "\n "))
|
|
366
|
-
|
|
371
|
+
if conduct:
|
|
372
|
+
print("\nWhat the record says of each author:")
|
|
373
|
+
for who, row in conduct.items():
|
|
374
|
+
print(f" {who:<10} {row['sentence']}")
|
|
375
|
+
print(f"\nBuilt {demo.TITLE!r} into {g.path}.\nExplore it: hypgraph --db {args.db} serve"
|
|
367
376
|
f" · hypgraph --db {args.db} report")
|
|
368
377
|
return 0
|
|
369
378
|
|
|
@@ -553,7 +562,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
553
562
|
"outside the hash chain")
|
|
554
563
|
s = cmd("import", cmd_import, "rebuild a graph file from a complete bundle")
|
|
555
564
|
s.add_argument("bundle")
|
|
556
|
-
cmd("demo", cmd_demo, "build
|
|
565
|
+
s = cmd("demo", cmd_demo, "build a teaching investigation: Broad Street (1854), or a disputed answer")
|
|
566
|
+
s.add_argument("name", nargs="?", default="broad-street", choices=["broad-street", "dispute"],
|
|
567
|
+
help="broad-street (default), or dispute: an honest mistake and a lie, told apart by the record")
|
|
557
568
|
cmd("schema", cmd_schema, "the node types and relations")
|
|
558
569
|
s = cmd("refusals", cmd_refusals, "the overclaims the record caught: per author, per rule, fixed or open")
|
|
559
570
|
s.add_argument("--inv", default="")
|