hypgraph 0.7.22__tar.gz → 0.7.23__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.22 → hypgraph-0.7.23}/CHANGELOG.md +32 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/PKG-INFO +1 -1
- {hypgraph-0.7.22 → hypgraph-0.7.23}/docs/PLAN.md +1 -1
- {hypgraph-0.7.22 → hypgraph-0.7.23}/docs/RELEASE-READINESS.md +17 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/__init__.py +1 -1
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/checks.py +144 -29
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/language.py +5 -1
- hypgraph-0.7.23/tests/test_direction_and_units.py +167 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/.gitignore +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/AGENTS.md +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/CITATION.cff +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/DISCLAIMER.md +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/LICENSE +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/NOTICE +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/README.md +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/docs/decisions/ADR-002-governance-profiles.md +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/docs/profile-workflows-plan.md +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/docs/review-2026-10-24.md +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/docs/site-plan.md +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/docs/spec.md +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/docs/ui-plan.md +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/docs/verification.md +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/docs/workflows/README.md +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/docs/workflows/knowledge.md +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/docs/workflows/optimization.md +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/docs/workflows/scientific.md +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/docs/workflows/software.md +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/docs/workflows/trading.md +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/examples/frameworks/README.md +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/pyproject.toml +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/__main__.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/blind.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/bundle.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/cli.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/coord.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/demos/__init__.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/demos/broad_street.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/demos/linked.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/derive.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/graph.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/mcp.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/plan.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/policy.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/profiles.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/references.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/refusals.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/report_check.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/schema.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/server.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/skill/SKILL.md +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/stats.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/store.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/ui/__init__.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/ui/atlas.css +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/ui/atlas.js +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/ui/hypgraph.css +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/ui/hypgraph.i18n.js +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/ui/hypgraph.js +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/ui/hypgraph.svg +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/ui/index.html +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/ui/vendor/cose-base.js +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/ui/vendor/layout-base.js +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/src/hypgraph/workflow.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_accept_work_kind.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_anchoring.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_blind.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_blind_oracles.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_bound_support.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_bundle_telemetry.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_cancel_releases_lease.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_cited_and_failed_support.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_contradictions.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_coord.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_d10.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_engine.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_engine_fixes.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_framework_examples.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_i18n.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_incremental.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_link_translate.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_linked_records.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_log_and_check.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_measure_questions.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_opposed_answers.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_option_lead.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_order_detail.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_profiles.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_profiles_041.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_questions_not_assertions.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_references.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_refusal_friction.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_refusal_results.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_refusals.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_relation_words.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_report_answers.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_rerunnable.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_restates_scale.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_server_auth.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_server_hosts.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_shape_refusals.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_stats_not_backtests.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_stats_values.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_surfaces.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_telemetry_stream.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_trust_boundary.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_ui_app.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_ui_engine.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_uncorrected_effect.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_unnamed_inputs.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_verify_complete.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_verify_lifecycle.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_verify_scope.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_verify_scoped_views.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_verify_trust.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_verify_views.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_versions_not_figures.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_vocabulary_friction.py +0 -0
- {hypgraph-0.7.22 → hypgraph-0.7.23}/tests/test_workflows.py +0 -0
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.23 — 2026-11-01: a count points one way, "no route is longer", what a unit is said of
|
|
4
|
+
|
|
5
|
+
Fixes from an independent audit of 0.7.22 (HG-022-01 to -05); each case below failed on 0.7.22 and is now a test.
|
|
6
|
+
|
|
7
|
+
- **A count points one way** (HG-022-01, -02). A count of routes longer than 26 km matched "the shortest route is
|
|
8
|
+
26 km" on the figure, so the true claim was refused as `COUNT_CONTRADICTS`; and a count of none shorter than 26
|
|
9
|
+
backed "nothing is longer than 26 km". Comparisons now carry a direction on an axis ("longer" / "shorter",
|
|
10
|
+
"faster" / "slower"; "more", "above", "highest" meet any axis), and a count that points the other way from the
|
|
11
|
+
claim's exceptions neither refutes nor backs it. A result can state the comparison with the count
|
|
12
|
+
(`{"k": 2, "n": 3, "figure": 26, "op": ">"}`); that is the only direction, and a measurement whose name points the
|
|
13
|
+
other way is refused as `MEASUREMENT_MISMATCH`. Without one, a name and key that point opposite ways can widen what
|
|
14
|
+
a count refutes, never what it backs.
|
|
15
|
+
- **"No route is longer than 26 km" is an absolute** (HG-022-02), like "nothing is longer": it needs a count of
|
|
16
|
+
exceptions. Only a "no" that opens its clause, followed within three words by a copula and a comparison, is read
|
|
17
|
+
this way ("routes that visit no town twice" is not one).
|
|
18
|
+
- **A unit of record said with a verb** (HG-022-03). Only "is / are / was / were" stated a unit, so "recorded parcel
|
|
19
|
+
weights use pounds" went in beside the note that every recorded parcel weight is in kilograms. A short, fixed list
|
|
20
|
+
is read too: use, recorded / measured / expressed / given / listed / reported / denominated / kept / stated /
|
|
21
|
+
written / logged / entered / priced / quoted in. A hedge ("might be recorded in pounds") asserts nothing.
|
|
22
|
+
- **Two things that share a word are two things** (HG-022-04). "Passenger weights are recorded in pounds" was refused
|
|
23
|
+
against a source saying shipping weights are in kilograms, on the word "weights". A unit is now compared only when
|
|
24
|
+
the two subjects have the same head and don't each carry modifiers the other lacks entirely; every clause of the
|
|
25
|
+
source is read, and one that says the claim's unit of that thing means no contradiction.
|
|
26
|
+
- **A quantity is not a unit of record** (HG-022-05). "The parcel mass is about 2.2 pounds", citing "the parcel mass
|
|
27
|
+
is one kilogram", was refused: a unit after a number ("2.2 pounds", "one kilogram", "36 kg") is a quantity, and
|
|
28
|
+
isn't compared. The refusal no longer says a quantity can't be in two units; it says the source doesn't support the
|
|
29
|
+
statement.
|
|
30
|
+
- Limits: which thing a subject names and which unit a record uses are read from words, not typed; conversions
|
|
31
|
+
aren't checked, so "2.2 pounds" against "50 kilograms" goes in; verbs outside the list aren't read; the "no X is
|
|
32
|
+
longer" pattern is narrow ("there is no route longer" isn't read); and a direction is read only from the comparison
|
|
33
|
+
words above.
|
|
34
|
+
|
|
3
35
|
## 0.7.22 — 2026-11-01: a label can't move a count, every part of a claim is checked, units rule each other out
|
|
4
36
|
|
|
5
37
|
Fixes from an independent audit of 0.7.21 (HG-021-01 to -04); each case below failed on 0.7.21 and is now a test.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hypgraph
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.23
|
|
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/
|
|
@@ -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.7.
|
|
112
|
+
## Status at 0.7.23 (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
|
|
@@ -114,6 +114,23 @@ was taken from a changelog without being checked against the code.*
|
|
|
114
114
|
> suite is 482 passed, and hypswarm (118 passed) and hypmesh (37 passed) pass on it. The audit asks for a fresh
|
|
115
115
|
> independent audit before anyone claims 0.7.22 resists these attack classes. Released as 0.7.22.
|
|
116
116
|
|
|
117
|
+
> **Audit of 0.7.22 (2026-11-01), fixed for 0.7.23.** A third independent audit found five cases, each reproduced on
|
|
118
|
+
> the 0.7.22 wheel. Two let a count stand for its opposite: a count of routes longer than 26 km refused the true "the
|
|
119
|
+
> shortest route is 26 km" (HG-022-01), and none shorter than 26 backed "nothing / no route is longer than 26 km"
|
|
120
|
+
> (HG-022-02). One let a unit claim in: "recorded parcel weights use pounds" stated no copula (HG-022-03). Two made
|
|
121
|
+
> false accusations: "passenger weights" was matched with "shipping weights" on a shared word (HG-022-04), and a
|
|
122
|
+
> quantity converted to another unit was read as a unit of record (HG-022-05). Comparisons now carry a direction, and
|
|
123
|
+
> a result can state its comparison (`op`); "no X is longer than…" opening a clause is an absolute; a short fixed list
|
|
124
|
+
> of verbs states a unit; units are compared only between subjects with the same head and compatible modifiers; a unit
|
|
125
|
+
> after a number is a quantity. Each case is a regression test written to fail on 0.7.22. Replaying the 1,900
|
|
126
|
+
> recorded adversarial entries names exactly the same parties as 0.7.22; one repeated false unit claim is no longer
|
|
127
|
+
> refused, because 0.7.22 had refused it on a shared word and a quantity, and its author is still named.
|
|
128
|
+
>
|
|
129
|
+
> **Where that stands.** #87 is merged with green CI (17 of 17 checks); from the 0.7.23 wheel in a fresh venv the
|
|
130
|
+
> suite is 504 passed, and hypswarm (118 passed) and hypmesh (37 passed) pass on it. Which thing a subject names and
|
|
131
|
+
> which unit a record uses are still read from words, not typed; a fresh independent audit should precede any claim
|
|
132
|
+
> that 0.7.23 resists these attack classes. Released as 0.7.23.
|
|
133
|
+
|
|
117
134
|
## Recommendation: CONDITIONAL GO
|
|
118
135
|
|
|
119
136
|
HypGraph is ready for a public developer preview once #59, #60 and #61 are merged with green CI and released as one
|
|
@@ -16,7 +16,7 @@ Standing is derived on every read, never stored; every write is an event in a SH
|
|
|
16
16
|
their numbers; reviews pin versions; independence is counted, not assumed. See https://hypgraph.com.
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
-
__version__ = "0.7.
|
|
19
|
+
__version__ = "0.7.23"
|
|
20
20
|
|
|
21
21
|
from .bundle import anchored, verify_bundle # noqa: E402
|
|
22
22
|
from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
|
|
@@ -213,6 +213,12 @@ def _bound(s: Snap, meas: list) -> tuple[list[tuple[dict, dict]], list[Problem]]
|
|
|
213
213
|
f"{said.group(0).strip()}, but result {m['result']} counted '{m['key']}' against "
|
|
214
214
|
f"{_fmt(f)}", fix=f"name the count after {_fmt(f)}, the figure it was made against"))
|
|
215
215
|
bad = True
|
|
216
|
+
op, named = stated_op(rv), _directions(str(m.get("name") or ""))
|
|
217
|
+
if not bad and op and any(_opposed(d, ("", _OPS[op])) for d in named):
|
|
218
|
+
problems.append(Problem("MEASUREMENT_MISMATCH", f"measurement {m.get('name')}: its name points the other "
|
|
219
|
+
f"way from what result {m['result']} counted for '{m['key']}' ({op})",
|
|
220
|
+
fix=f"name the count after the comparison it made ({op})"))
|
|
221
|
+
bad = True
|
|
216
222
|
if not bad:
|
|
217
223
|
good.append((m, rv))
|
|
218
224
|
return good, problems
|
|
@@ -301,6 +307,74 @@ def _count_figures(m: dict, rv: dict, figs: set[float] = frozenset()) -> set[flo
|
|
|
301
307
|
return out
|
|
302
308
|
|
|
303
309
|
|
|
310
|
+
# which way a comparison points, as (axis, sign): "longer" and "longest" point up the length axis, "shorter" and
|
|
311
|
+
# "shortest" down it; "more", "above", "highest" point up with no axis of their own, so they meet any axis. A count of
|
|
312
|
+
# routes longer than 26 km says nothing against "the shortest is 26 km" (audit HG-022-01), and none shorter than 26
|
|
313
|
+
# doesn't back "nothing is longer than 26" (HG-022-02).
|
|
314
|
+
_DIRECTION_WORDS = {
|
|
315
|
+
"": {1: "more greater higher larger bigger above over exceed exceeds exceeding exceeded most highest largest "
|
|
316
|
+
"greatest biggest maximum max top",
|
|
317
|
+
-1: "less fewer lower smaller below under least lowest smallest fewest minimum min"},
|
|
318
|
+
"long": {1: "longer longest", -1: "shorter shortest"},
|
|
319
|
+
"good": {1: "better best", -1: "worse worst"},
|
|
320
|
+
"fast": {1: "faster fastest", -1: "slower slowest"},
|
|
321
|
+
"cost": {1: "dearer dearest", -1: "cheaper cheapest"},
|
|
322
|
+
"time": {1: "later latest", -1: "earlier earliest"},
|
|
323
|
+
}
|
|
324
|
+
_DIRECTION_OF = {w: (axis, sign) for axis, signs in _DIRECTION_WORDS.items() for sign, ws in signs.items()
|
|
325
|
+
for w in ws.split()}
|
|
326
|
+
_OPS = {">": 1, ">=": 1, "<": -1, "<=": -1}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _directions(text: str) -> set[tuple[str, int]]:
|
|
330
|
+
return {_DIRECTION_OF[w] for w in re.findall(r"[a-z]+", (text or "").lower()) if w in _DIRECTION_OF}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _opposed(a: tuple[str, int], b: tuple[str, int]) -> bool:
|
|
334
|
+
"""Do these point opposite ways on one axis? An axis-free word ("more") meets any axis; two different axes
|
|
335
|
+
("longer", "faster") can't be compared, so they aren't opposed."""
|
|
336
|
+
return a[1] != b[1] and (a[0] == b[0] or not a[0] or not b[0])
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def stated_op(rv: dict) -> str | None:
|
|
340
|
+
"""The comparison a count says it made, written by the instrument beside it: {"k": 2, "n": 3, "figure": 26,
|
|
341
|
+
"op": ">"} is two found above 26."""
|
|
342
|
+
op = rv.get("op")
|
|
343
|
+
return op if op in _OPS else None
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _count_directions(m: dict, rv: dict) -> set[tuple[str, int]]:
|
|
347
|
+
"""Every way a bound count could point; empty when nothing says. A comparison the result states is the only one.
|
|
348
|
+
Otherwise its name's and its key's: as with its figure, the author writes the name, so a name that points the
|
|
349
|
+
other way from the key adds a direction and never replaces one."""
|
|
350
|
+
op = stated_op(rv)
|
|
351
|
+
if op:
|
|
352
|
+
return {("", _OPS[op])}
|
|
353
|
+
return _directions(str(m.get("name") or "")) | _directions(str(m.get("key") or "").replace("_", " "))
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _claim_directions(stmt: str, w: str = "") -> set[tuple[str, int]]:
|
|
357
|
+
"""Which way the exceptions to a claim point: those the absolute ``w``'s own clause compares against ("nothing
|
|
358
|
+
longer than 26 km" — longer), else those beyond its extremes ("the shortest route is 26 km" — shorter)."""
|
|
359
|
+
out: set[tuple[str, int]] = set()
|
|
360
|
+
for m in re.finditer(r"\b" + re.escape(w) + r"\b", stmt, re.I) if w else ():
|
|
361
|
+
end = next((x.start() for x in _SUBCLAUSE.finditer(stmt, m.end())), len(stmt))
|
|
362
|
+
out |= {d for h in _CLAIM_FIGURE.finditer(stmt[m.end():end]) for d in _directions(h.group(0))}
|
|
363
|
+
return out or {d for x in _extremes(stmt) for d in _directions(x)}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _points(m: dict, rv: dict, a: tuple[int, int], dirs: set[tuple[str, int]]) -> bool:
|
|
367
|
+
"""Does this count point the way the claim's exceptions do? One that points the other way can't refute the claim
|
|
368
|
+
or back it. When it could point either way (a name against its key), a count that finds some may still refute,
|
|
369
|
+
and a count of none or all backs the claim only if no way it could point is the other way. A count, or a claim,
|
|
370
|
+
whose direction can't be told is read as before."""
|
|
371
|
+
c = _count_directions(m, rv)
|
|
372
|
+
if not c or not dirs:
|
|
373
|
+
return True
|
|
374
|
+
against = {d for d in c if all(_opposed(d, x) for x in dirs)}
|
|
375
|
+
return against != c if 0 < a[0] < a[1] else not against
|
|
376
|
+
|
|
377
|
+
|
|
304
378
|
def _absolute_figures(stmt: str, w: str) -> set[float]:
|
|
305
379
|
"""The figures an absolute is about: one its own clause compares against ("nothing worth more than 93 fits"),
|
|
306
380
|
else the figures the statement's extremes name ("the highest is 93; nothing worth more fits")."""
|
|
@@ -311,12 +385,15 @@ def _absolute_figures(stmt: str, w: str) -> set[float]:
|
|
|
311
385
|
return figs or _extreme_figures(stmt)
|
|
312
386
|
|
|
313
387
|
|
|
314
|
-
def _bears(m: dict, rv: dict, a: tuple[int, int], figs: set[float]
|
|
388
|
+
def _bears(m: dict, rv: dict, a: tuple[int, int], figs: set[float],
|
|
389
|
+
dirs: set[tuple[str, int]] = frozenset()) -> bool:
|
|
315
390
|
"""Does this count bear on a claim about ``figs``? One about another figure doesn't, whatever it finds: a zero
|
|
316
391
|
beyond 140 can't vouch for 93, and two beyond 93 can't refute 95. When it could be about more than one figure (a
|
|
317
392
|
name that disagrees with its key), a count that finds some bears if any of them is the claim's, and a count of
|
|
318
393
|
none or all only if every one is: a label can widen what a count refutes, never what it vouches for. A count that
|
|
319
394
|
names no figure, or a claim that names none, is read as before."""
|
|
395
|
+
if not _points(m, rv, a, dirs):
|
|
396
|
+
return False
|
|
320
397
|
c = _count_figures(m, rv, figs)
|
|
321
398
|
if not c or not figs:
|
|
322
399
|
return True
|
|
@@ -349,7 +426,8 @@ def _absolutes(stmt: str, scope: str, body: dict, bound: list) -> list[Problem]:
|
|
|
349
426
|
|
|
350
427
|
|
|
351
428
|
def _absolute(stmt: str, scope: str, body: dict, bound: list, w: str, words: list, figs: set[float]) -> list[Problem]:
|
|
352
|
-
|
|
429
|
+
dirs = _claim_directions(stmt, w)
|
|
430
|
+
arms = [(m, rv, a) for m, rv in bound if (a := _arm(rv)) and _bears(m, rv, a, figs, dirs)]
|
|
353
431
|
about = L.content_words(stmt)
|
|
354
432
|
# a count at the claim's own figure that finds exceptions refutes it, whatever else is bound beside it
|
|
355
433
|
pinned = [(m, a) for m, rv, a in arms if _pinned(m, rv, a, figs)
|
|
@@ -506,7 +584,8 @@ def _superlative_contradicted(stmt: str, bound: list) -> list[Problem]:
|
|
|
506
584
|
return []
|
|
507
585
|
about = L.content_words(stmt)
|
|
508
586
|
figs = _extreme_figures(stmt)
|
|
509
|
-
|
|
587
|
+
dirs = _claim_directions(stmt)
|
|
588
|
+
rel = [(m, rv, a) for m, rv in bound if (a := _arm(rv)) and _bears(m, rv, a, figs, dirs)
|
|
510
589
|
and (L.content_words(f"{m.get('name', '')} {m.get('key', '')}") & about
|
|
511
590
|
or _BEYOND.search(f"{m.get('name', '')} {m.get('key', '')}".replace("_", " "))
|
|
512
591
|
or stated_figure(rv) in figs)]
|
|
@@ -720,22 +799,60 @@ def _plural(canon: str) -> str:
|
|
|
720
799
|
"yen": "yen", "yuan": "yuan"}.get(canon, canon + "s")
|
|
721
800
|
|
|
722
801
|
|
|
723
|
-
|
|
724
|
-
|
|
802
|
+
# a unit after a number is a quantity ("about 2.2 pounds", "one kilogram", "36 kg"), not the unit something is
|
|
803
|
+
# recorded in: a quantity converted into another unit says nothing against a source's unit (audit HG-022-05)
|
|
804
|
+
_QUANTITY = re.compile(r"(?:\d|\b(?:a|an|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|twenty|thirty|"
|
|
805
|
+
r"forty|fifty|hundred|hundreds|thousand|thousands|million|millions|dozen|dozens|half|quarter|"
|
|
806
|
+
r"few|several|couple)\b)[\s-]*(?:of\s+)?$", re.I)
|
|
725
807
|
|
|
726
808
|
|
|
727
|
-
|
|
809
|
+
def _units_in(text: str) -> set[tuple[str, str]]:
|
|
810
|
+
"""The units of record ``text`` names: a unit a number comes before is a quantity and doesn't count."""
|
|
811
|
+
text = (text or "").lower()
|
|
812
|
+
return {u for m in re.finditer(r"[a-z]+", text) if m.group(0) in _UNIT_OF
|
|
813
|
+
and not _QUANTITY.search(text[:m.start()]) for u in _UNIT_OF[m.group(0)]}
|
|
728
814
|
|
|
729
815
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
816
|
+
_CONDITIONAL = re.compile(r"\b(?:if|whether|unless|suppos\w*|assum\w*|were they|had they)\b", re.I)
|
|
817
|
+
# what states the unit something is in: a copula, or one of a short, fixed list of verbs ("recorded parcel weights use
|
|
818
|
+
# pounds", "the weights are given in pounds"; audit HG-022-03). Prose outside the list isn't read.
|
|
819
|
+
_RECORDS = re.compile(r"\b(?:is|are|was|were|uses?|used|using|(?:recorded|measured|expressed|given|listed|reported|"
|
|
820
|
+
r"denominated|kept|stated|written|logged|entered|priced|quoted)\s+(?:in|as|using))\b", re.I)
|
|
821
|
+
_HEDGED = re.compile(r"\b(?:may|might|could|perhaps|possibly)\b", re.I)
|
|
822
|
+
_DETERMINERS = set("a an the all every each any some these those this that its their our his her both".split())
|
|
823
|
+
_PREPOSITIONS = set("of in for from on at by with to among across within under over into per".split())
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def _subject(text: str) -> tuple[str, frozenset[str]]:
|
|
827
|
+
"""What a unit is said of, as (head, modifiers): "every recorded parcel weight in the register" is (weight,
|
|
828
|
+
{recorded, parcel}). Determiners go, and the subject ends at its first preposition."""
|
|
829
|
+
words = []
|
|
830
|
+
for w in re.findall(r"[a-z]+", (text or "").lower()):
|
|
831
|
+
if w in _PREPOSITIONS and words:
|
|
832
|
+
break
|
|
833
|
+
if w not in _DETERMINERS:
|
|
834
|
+
words += sorted(L.content_words(w))
|
|
835
|
+
return (words[-1], frozenset(words[:-1])) if words else ("", frozenset())
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def _same_thing(a: tuple[str, frozenset[str]], b: tuple[str, frozenset[str]]) -> bool:
|
|
839
|
+
"""Could these be one thing? Their heads must match, and two subjects each named for something the other isn't
|
|
840
|
+
("passenger weights", "shipping weights") are two things (audit HG-022-04). A bare "weights" may be either."""
|
|
841
|
+
return bool(a[0]) and a[0] == b[0] and (not a[1] or not b[1] or bool(a[1] & b[1]))
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def _unit_assertions(text: str) -> list[tuple[tuple[str, frozenset[str]], set[tuple[str, str]], str]]:
|
|
845
|
+
"""(subject, units of record asserted of it, clause) for each clause that states one, outside the reach of
|
|
846
|
+
negation: "the recorded weights are in pounds" says pounds of the recorded weights. A condition ("if the weights
|
|
847
|
+
are pounds") or a hedge ("the weights might be recorded in pounds") asserts nothing."""
|
|
734
848
|
out = []
|
|
735
849
|
for c in _UNIT_CLAUSE.split(_unnegated(text or "")):
|
|
736
|
-
|
|
737
|
-
if
|
|
738
|
-
|
|
850
|
+
verb = _RECORDS.search(c)
|
|
851
|
+
if not verb or _CONDITIONAL.search(c[:verb.start()]) or _HEDGED.search(c[:verb.start()]):
|
|
852
|
+
continue
|
|
853
|
+
subj = _subject(c[:verb.start()])
|
|
854
|
+
if subj[0] and (units := _units_in(c[verb.end():])):
|
|
855
|
+
out.append((subj, units, c.strip()))
|
|
739
856
|
return out
|
|
740
857
|
|
|
741
858
|
|
|
@@ -746,25 +863,23 @@ def asserted_units(stmt: str) -> set[str]:
|
|
|
746
863
|
|
|
747
864
|
|
|
748
865
|
def _other_unit(passage: str, stmt: str) -> tuple[str, str, str] | None:
|
|
749
|
-
"""(the statement's unit, the source's, the source sentence) when
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
denial, checked above)."""
|
|
866
|
+
"""(the statement's unit, the source's, the source sentence) when the source says the statement's subject is in
|
|
867
|
+
another unit of the same kind, nowhere says it is in the statement's, and no clause of the statement about that
|
|
868
|
+
subject says the source's unit too ("kilograms, not pounds" is a denial, checked above)."""
|
|
753
869
|
claimed = _unit_assertions(stmt)
|
|
754
870
|
if not claimed:
|
|
755
871
|
return None
|
|
756
|
-
for sent in re.split(r"(?<=[.!?;])\s+", passage or "")
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
if
|
|
760
|
-
|
|
761
|
-
subj,
|
|
762
|
-
|
|
763
|
-
also = {u for units in about for u in units}
|
|
764
|
-
for fam, canon in sorted(also):
|
|
872
|
+
given = [(subj, units, sent.strip()) for sent in re.split(r"(?<=[.!?;])\s+", passage or "")
|
|
873
|
+
for subj, units, _c in _unit_assertions(sent)]
|
|
874
|
+
for csubj, cunits, _c in claimed:
|
|
875
|
+
theirs_all = [(units, sent) for subj, units, sent in given if _same_thing(csubj, subj)]
|
|
876
|
+
said = {u for units, _s in theirs_all for u in units}
|
|
877
|
+
also = {u for subj, units, _c in claimed if _same_thing(csubj, subj) for u in units}
|
|
878
|
+
for fam, canon in sorted(cunits):
|
|
765
879
|
theirs = {c for f, c in said if f == fam}
|
|
766
880
|
if theirs and canon not in theirs and not {(fam, c) for c in theirs} & also:
|
|
767
|
-
|
|
881
|
+
sent = next(s for units, s in theirs_all if any(f == fam for f, _c in units))
|
|
882
|
+
return canon, sorted(theirs)[0], sent
|
|
768
883
|
return None
|
|
769
884
|
|
|
770
885
|
|
|
@@ -805,8 +920,8 @@ def _source_contradicts(s: Snap, stmt: str, support: list[str]) -> list[Problem]
|
|
|
805
920
|
label = cite[:80] if cite else (n.get("code") or src)
|
|
806
921
|
return [Problem("SOURCE_CONTRADICTS",
|
|
807
922
|
f"the statement says {_plural(ours)}, but a source it links as support ({label}) says "
|
|
808
|
-
f"{_plural(theirs)}: \"{sent[:200]}\".
|
|
809
|
-
"
|
|
923
|
+
f"{_plural(theirs)} for the same thing: \"{sent[:200]}\". That source doesn't support the statement: "
|
|
924
|
+
"correct the statement, or link the source as a challenge",
|
|
810
925
|
fix="correct the statement, or link that source as a challenge", words=[f"unit:{ours}"])]
|
|
811
926
|
return []
|
|
812
927
|
|
|
@@ -18,7 +18,11 @@ ABSOLUTE = re.compile(
|
|
|
18
18
|
r"\b(never|always|all|every|none|nothing|no one|nobody|impossible|cannot|can't|proves?|proven|proof|certainly|"
|
|
19
19
|
r"certain|guaranteed?|guarantees|eliminat(?:es|ed|ing)|entirely|completely|totally|universally|invariably|"
|
|
20
20
|
r"consistently|without exception|perfect(?:ly)?|solves?|solved|definitively|conclusively|undeniably|"
|
|
21
|
-
r"zero(?!-)
|
|
21
|
+
r"zero(?!-)"
|
|
22
|
+
# "no route is longer than 26 km": a universal negative comparison, the same claim as "nothing is longer"; only
|
|
23
|
+
# where "no" opens its clause ("routes that visit no town twice" is not one)
|
|
24
|
+
r"|(?<![a-z] )no(?=\s+(?:[a-z]+\s+){1,3}?(?:is|are|was|were)\s+(?:any\s+)?(?:longer|shorter|more|less|fewer|greater|higher|"
|
|
25
|
+
r"lower|larger|smaller|better|worse|faster|slower|cheaper|above|below|over|under)\b))\b|(?<![\d.])100\s*%", re.I)
|
|
22
26
|
# a superlative names an extreme ("the highest value is 93"): a count of anything beyond it contradicts it
|
|
23
27
|
SUPERLATIVE = re.compile(
|
|
24
28
|
r"\b(highest|lowest|largest|smallest|shortest|longest|greatest|fewest|best|worst|maximum|minimum|optimal|"
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Which way a count points, and what a unit says (audit of 0.7.22, HG-022-01..05).
|
|
2
|
+
|
|
3
|
+
A count of routes longer than 26 km says nothing against "the shortest route is 26 km", and a count of none shorter
|
|
4
|
+
than 26 doesn't back "nothing is longer than 26". A source's unit of record is compared only with the same thing's
|
|
5
|
+
unit of record: not with another thing that shares a word, and not with a quantity converted into another unit."""
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from hypgraph import Graph, GraphError, Policy
|
|
9
|
+
|
|
10
|
+
ROUTES = "The three surveyed routes, of 26, 31 and 35 km."
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def record(values):
|
|
14
|
+
g = Graph(policy=Policy(reviewers={"critic"}))
|
|
15
|
+
inv = g.author("curator").investigation("Routes", "Which surveyed route is shortest?")["id"]
|
|
16
|
+
lab = g.author("lab")
|
|
17
|
+
run = lab.add("run", inv, kind="lab", state="succeeded", name="Measured the surveyed routes.")
|
|
18
|
+
r = lab.add("result", inv, kind="computed", outcome="positive", summary="Route counts against 26 km.",
|
|
19
|
+
values=values, links=[{"rel": "produced", "from": run["id"]}])["id"]
|
|
20
|
+
return g, inv, r
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def source(g, inv, passage):
|
|
24
|
+
return g.author("curator").add("source", inv, citation="Records note", passage=passage, kind="primary")["id"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def write(g, inv, statement, links, meas=(), scope=ROUTES, who="a"):
|
|
28
|
+
return g.author(who).add("claim", inv, statement=statement, scope=scope, measurements=list(meas),
|
|
29
|
+
links=[{"rel": "supports", "from": x, "why": "Cited for the claim."} for x in links])
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def codes(e) -> list[str]:
|
|
33
|
+
return [p["code"] for p in e.value.problems]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def count(r, key, name, k, n=3):
|
|
37
|
+
return {"name": name, "result": r, "key": key, "k": k, "n": n}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
SHORTEST = {"name": "shortest route km", "key": "shortest_km", "value": 26}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.parametrize("typed", [{}, {"figure": 26}, {"figure": 26, "op": ">"}])
|
|
44
|
+
def test_a_count_the_other_way_does_not_refute_a_true_extreme(typed):
|
|
45
|
+
"""HG-022-01: the true "the shortest route is 26 km", with 2 of 3 routes longer than 26 bound, was refused as
|
|
46
|
+
COUNT_CONTRADICTS: the count matched the figure, and nothing asked which way it pointed."""
|
|
47
|
+
g, inv, r = record({"longer_than_26": {"k": 2, "n": 3, **typed}, "shortest_km": 26})
|
|
48
|
+
write(g, inv, "The shortest route is 26 km.", [r],
|
|
49
|
+
[count(r, "longer_than_26", "routes longer than 26 km", 2), {**SHORTEST, "result": r}])
|
|
50
|
+
assert "a" not in g.conduct(inv)
|
|
51
|
+
# the count the claim needs still decides: 1 of 3 shorter than 31 refutes "the shortest is 31"
|
|
52
|
+
g, inv, r = record({"shorter_than_31": {"k": 1, "n": 3, **({"figure": 31} if typed else {})}, "x_km": 31})
|
|
53
|
+
with pytest.raises(GraphError) as e:
|
|
54
|
+
write(g, inv, "The shortest route is 31 km.", [r],
|
|
55
|
+
[count(r, "shorter_than_31", "routes shorter than 31 km", 1),
|
|
56
|
+
{"name": "route km", "result": r, "key": "x_km", "value": 31}])
|
|
57
|
+
assert "COUNT_CONTRADICTS" in codes(e)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.parametrize("typed", [{}, {"figure": 26}, {"figure": 26, "op": "<"}])
|
|
61
|
+
@pytest.mark.parametrize("statement", ["Nothing is longer than 26 km among the 3 routes.",
|
|
62
|
+
"No route is longer than 26 km among the 3 routes."])
|
|
63
|
+
def test_a_count_the_other_way_does_not_back_an_absolute(typed, statement):
|
|
64
|
+
"""HG-022-02: none of 3 routes shorter than 26 backed "nothing is longer than 26 km among the 3 routes". It is
|
|
65
|
+
the wrong count: the claim goes in only with a count of routes longer than 26, which here finds 2."""
|
|
66
|
+
g, inv, r = record({"shorter_than_26": {"k": 0, "n": 3, **typed}, "longer_than_26": {"k": 2, "n": 3}})
|
|
67
|
+
with pytest.raises(GraphError) as e:
|
|
68
|
+
write(g, inv, statement, [r], [count(r, "shorter_than_26", "routes shorter than 26 km", 0)])
|
|
69
|
+
assert "ABSOLUTE_WITHOUT_COUNT" in codes(e) and "COUNT_CONTRADICTS" not in codes(e)
|
|
70
|
+
with pytest.raises(GraphError) as e:
|
|
71
|
+
write(g, inv, statement, [r], [count(r, "shorter_than_26", "routes shorter than 26 km", 0),
|
|
72
|
+
count(r, "longer_than_26", "routes longer than 26 km", 2)])
|
|
73
|
+
assert "COUNT_CONTRADICTS" in codes(e)
|
|
74
|
+
# the true absolute with its own count goes in
|
|
75
|
+
write(g, inv, "No route is shorter than 26 km among the 3 routes.", [r],
|
|
76
|
+
[count(r, "shorter_than_26", "routes shorter than 26 km", 0)])
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_a_no_inside_a_phrase_is_not_an_absolute():
|
|
80
|
+
""""no route is longer" opens its clause with "no"; "routes that visit no town twice" only describes them. A true
|
|
81
|
+
count sentence with that phrase in it goes in."""
|
|
82
|
+
g, inv, r = record({"shorter_than_31": {"k": 1, "n": 3}})
|
|
83
|
+
write(g, inv, "One of the 3 routes that visit no town twice is shorter than 31 km.", [r],
|
|
84
|
+
[count(r, "shorter_than_31", "routes shorter than 31 km", 1)])
|
|
85
|
+
assert "a" not in g.conduct(inv)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_a_stated_direction_decides_and_a_label_cannot_flip_it():
|
|
89
|
+
"""A result can state which way its count points ({"op": "<"}); a measurement whose name points the other way is
|
|
90
|
+
refused. Without one, a name and key that point opposite ways can widen what the count refutes, never what it
|
|
91
|
+
backs."""
|
|
92
|
+
g, inv, r = record({"shorter_than_26": {"k": 0, "n": 3, "figure": 26, "op": "<"},
|
|
93
|
+
"beyond_26": {"k": 0, "n": 3}})
|
|
94
|
+
with pytest.raises(GraphError) as e:
|
|
95
|
+
write(g, inv, "Nothing is longer than 26 km among the 3 routes.", [r],
|
|
96
|
+
[count(r, "shorter_than_26", "routes longer than 26 km", 0)])
|
|
97
|
+
assert "MEASUREMENT_MISMATCH" in codes(e) and "<" in str(e.value)
|
|
98
|
+
# untyped: the name says longer, the key says shorter: a zero can't back "nothing longer"
|
|
99
|
+
g, inv, r = record({"shorter_than_26": {"k": 0, "n": 3}})
|
|
100
|
+
with pytest.raises(GraphError) as e:
|
|
101
|
+
write(g, inv, "Nothing is longer than 26 km among the 3 routes.", [r],
|
|
102
|
+
[count(r, "shorter_than_26", "routes longer than 26 km", 0)])
|
|
103
|
+
assert "ABSOLUTE_WITHOUT_COUNT" in codes(e)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_the_direction_of_words():
|
|
107
|
+
from hypgraph.checks import _claim_directions, _count_directions, _opposed
|
|
108
|
+
d = lambda name, key="x", rv=None: _count_directions({"name": name, "key": key}, rv or {}) # noqa: E731
|
|
109
|
+
assert d("routes longer than 26 km") == {("long", 1)}
|
|
110
|
+
assert d("routes shorter than 26 km") == {("long", -1)}
|
|
111
|
+
assert d("fitting selections worth more than 93") == {("", 1)}
|
|
112
|
+
assert d("fitting selections worth exactly 93") == set()
|
|
113
|
+
assert d("x", rv={"op": "<="}) == {("", -1)}
|
|
114
|
+
assert d("routes longer than 26 km", "shorter_than_26") == {("long", 1), ("long", -1)}
|
|
115
|
+
assert _claim_directions("The shortest route is 26 km.") == {("long", -1)}
|
|
116
|
+
assert _claim_directions("The highest total value that fits is 93.") == {("", 1)}
|
|
117
|
+
assert _claim_directions("Nothing worth more than 93 fits.", "Nothing") == {("", 1)}
|
|
118
|
+
assert _opposed(("long", 1), ("long", -1)) and _opposed(("", 1), ("long", -1))
|
|
119
|
+
assert not _opposed(("long", 1), ("fast", -1)) and not _opposed(("", 1), ("", 1))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
UNITS = "Every recorded parcel weight is in kilograms, not pounds."
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@pytest.mark.parametrize("statement", ["Recorded parcel weights use pounds.",
|
|
126
|
+
"The recorded parcel weights are given in pounds.",
|
|
127
|
+
"Recorded parcel weights were logged in pounds.",
|
|
128
|
+
"The recorded parcel weights are expressed in pounds."])
|
|
129
|
+
def test_a_unit_of_record_said_without_a_copula(statement):
|
|
130
|
+
"""HG-022-03: only "is / are / was / were" said a unit, so "recorded parcel weights use pounds" went in beside
|
|
131
|
+
the kilograms note. A short, fixed list of verbs that state a unit of record is read too."""
|
|
132
|
+
g, inv, _r = record({"x": {"k": 0, "n": 3}})
|
|
133
|
+
with pytest.raises(GraphError) as e:
|
|
134
|
+
write(g, inv, statement, [source(g, inv, UNITS)], scope="The parcel register.")
|
|
135
|
+
assert "SOURCE_CONTRADICTS" in codes(e)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@pytest.mark.parametrize("passage", [
|
|
139
|
+
"Shipping weights are recorded in kilograms; passenger weights are recorded in pounds.",
|
|
140
|
+
"Passenger weights are recorded in pounds; shipping weights are recorded in kilograms.",
|
|
141
|
+
"Shipping weights are recorded in kilograms."])
|
|
142
|
+
def test_another_thing_that_shares_a_word_is_not_contradicted(passage):
|
|
143
|
+
"""HG-022-04: "passenger weights" was matched with "shipping weights" on the word "weights", and the true claim
|
|
144
|
+
was refused as going against its source. Two things each named for something the other isn't are two things."""
|
|
145
|
+
g, inv, _r = record({"x": {"k": 0, "n": 3}})
|
|
146
|
+
write(g, inv, "Passenger weights are recorded in pounds.", [source(g, inv, passage)], scope="The register.")
|
|
147
|
+
assert "a" not in g.conduct(inv)
|
|
148
|
+
# the same thing still is: "recorded shipping weights" against shipping weights in kilograms
|
|
149
|
+
with pytest.raises(GraphError) as e:
|
|
150
|
+
write(g, inv, "The recorded shipping weights are in pounds.",
|
|
151
|
+
[source(g, inv, "Shipping weights are recorded in kilograms.")], scope="The register.")
|
|
152
|
+
assert "SOURCE_CONTRADICTS" in codes(e)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@pytest.mark.parametrize("statement", ["The parcel mass is approximately two pounds.",
|
|
156
|
+
"The parcel mass is about 2.2 pounds.",
|
|
157
|
+
"The parcel mass is a little over two pounds."])
|
|
158
|
+
def test_a_quantity_in_another_unit_is_not_a_unit_of_record(statement):
|
|
159
|
+
"""HG-022-05: "the parcel mass is about 2.2 pounds", citing "the parcel mass is one kilogram", was refused: a
|
|
160
|
+
quantity converted to another unit was read as a claim about which unit the records use. A unit after a number is
|
|
161
|
+
a quantity; conversions are not checked."""
|
|
162
|
+
g, inv, _r = record({"x": {"k": 0, "n": 3}})
|
|
163
|
+
try:
|
|
164
|
+
write(g, inv, statement, [source(g, inv, "The parcel mass is one kilogram.")],
|
|
165
|
+
scope="One parcel; 1 kg is about 2.2 pounds.")
|
|
166
|
+
except GraphError as e:
|
|
167
|
+
assert "SOURCE_CONTRADICTS" not in [p["code"] for p in e.problems], e.problems
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|