hypgraph 0.7.26__tar.gz → 0.7.27__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.26 → hypgraph-0.7.27}/CHANGELOG.md +23 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/PKG-INFO +1 -1
- {hypgraph-0.7.26 → hypgraph-0.7.27}/docs/PLAN.md +1 -1
- {hypgraph-0.7.26 → hypgraph-0.7.27}/docs/RELEASE-READINESS.md +15 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/__init__.py +1 -1
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/checks.py +73 -25
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/language.py +13 -5
- hypgraph-0.7.27/tests/test_borrowed_words.py +130 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/.gitignore +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/AGENTS.md +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/CITATION.cff +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/DISCLAIMER.md +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/LICENSE +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/NOTICE +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/README.md +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/docs/decisions/ADR-002-governance-profiles.md +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/docs/profile-workflows-plan.md +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/docs/review-2026-10-24.md +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/docs/site-plan.md +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/docs/spec.md +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/docs/ui-plan.md +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/docs/verification.md +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/docs/workflows/README.md +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/docs/workflows/knowledge.md +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/docs/workflows/optimization.md +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/docs/workflows/scientific.md +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/docs/workflows/software.md +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/docs/workflows/trading.md +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/examples/frameworks/README.md +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/pyproject.toml +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/__main__.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/blind.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/bundle.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/cli.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/coord.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/demos/__init__.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/demos/broad_street.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/demos/linked.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/derive.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/graph.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/mcp.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/plan.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/policy.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/profiles.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/references.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/refusals.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/report_check.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/schema.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/server.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/skill/SKILL.md +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/stats.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/store.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/ui/__init__.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/ui/atlas.css +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/ui/atlas.js +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/ui/hypgraph.css +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/ui/hypgraph.i18n.js +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/ui/hypgraph.js +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/ui/hypgraph.svg +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/ui/index.html +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/ui/vendor/cose-base.js +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/ui/vendor/layout-base.js +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/src/hypgraph/workflow.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_accept_work_kind.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_anchoring.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_blind.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_blind_oracles.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_bound_support.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_bundle_telemetry.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_cancel_releases_lease.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_cited_and_failed_support.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_contradictions.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_coord.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_counts_that_bear.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_d10.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_direction_and_units.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_engine.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_engine_fixes.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_framework_examples.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_i18n.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_incremental.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_link_translate.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_linked_records.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_log_and_check.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_measure_questions.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_opposed_answers.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_option_lead.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_order_detail.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_profiles.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_profiles_041.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_quantifiers_and_identity.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_questions_not_assertions.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_references.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_refusal_friction.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_refusal_results.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_refusals.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_relation_words.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_report_answers.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_rerunnable.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_restates_scale.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_server_auth.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_server_hosts.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_shape_refusals.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_stats_not_backtests.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_stats_values.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_surfaces.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_telemetry_stream.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_trust_boundary.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_ui_app.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_ui_engine.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_uncorrected_effect.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_unnamed_inputs.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_verify_complete.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_verify_lifecycle.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_verify_scope.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_verify_scoped_views.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_verify_trust.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_verify_views.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_versions_not_figures.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_vocabulary_friction.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_what_a_count_is_of.py +0 -0
- {hypgraph-0.7.26 → hypgraph-0.7.27}/tests/test_workflows.py +0 -0
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.27 — 2026-11-01: words said elsewhere, comparisons the checks didn't know
|
|
4
|
+
|
|
5
|
+
Fixes from an independent audit of 0.7.26 (HG-026-01, -02); each case below failed on 0.7.26 and is now a test
|
|
6
|
+
(`tests/test_borrowed_words.py`).
|
|
7
|
+
|
|
8
|
+
- **A word said in another clause doesn't make a count fit** (HG-026-01). What a count's key says it counted had to be
|
|
9
|
+
said by the measurement's name or anywhere in the statement, so "Speed was recorded separately; no route is longer
|
|
10
|
+
than 26 km" let a speed count named "routes longer than 26 km" back the length claim, and "Cargo routes were
|
|
11
|
+
inspected; no passenger route is longer …" did the same for a cargo count. The words must now be said where the
|
|
12
|
+
claim states the count's figure (its clause, from the comma before its absolute or extreme). Eight paraphrases over
|
|
13
|
+
other subjects and properties, written after the fix, are tests too: an extra clause never turns a refusal into an
|
|
14
|
+
acceptance, and an honest count backs the claim with or without one.
|
|
15
|
+
- **Any comparison makes an absolute** (HG-026-01, found while reproducing it). "No crate is heavier than 40 kg" and "no
|
|
16
|
+
shift had errors above 5" weren't read as absolutes, so they went in with no count, or with a count of crate prices.
|
|
17
|
+
"No …" before any comparison word is now an absolute; heavier / lighter, warmer / colder, older / younger, taller,
|
|
18
|
+
wider, deeper, louder and stronger are comparisons along their own axes (weight, temperature, age, height, width,
|
|
19
|
+
depth, loudness, strength), and a comparative the checks don't know ("brighter than") is met only by a count that
|
|
20
|
+
says the same word. A three-letter word before a comparison ("bridge age above 60") now names what was counted, and
|
|
21
|
+
"span" is a length, so an honest count of bridge spans backs "no bridge is longer than 60 m".
|
|
22
|
+
- **A mixed "and" / "or" clause has to be split** (HG-026-02). 0.7.26's notes said such a clause is unresolved "until a
|
|
23
|
+
count of the whole condition is bound"; nothing ties a count to the whole condition, so that never happened. The
|
|
24
|
+
refusal now says to split the claim into clauses that each join their comparisons one way.
|
|
25
|
+
|
|
3
26
|
## 0.7.26 — 2026-11-01: what a count is of, what its key compared, "and" with "or"
|
|
4
27
|
|
|
5
28
|
Fixes from an independent audit of 0.7.25 (HG-025-01 to -03); each case below failed on 0.7.25 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.27
|
|
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.27 (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
|
|
@@ -181,6 +181,21 @@ was taken from a changelog without being checked against the code.*
|
|
|
181
181
|
> **Where that stands.** #96 is merged with green CI (17 of 17 checks). Kinds and subjects are still read from the
|
|
182
182
|
> words around a comparison, a heuristic; the typed claim layer the audits recommend is not built. Released as 0.7.26.
|
|
183
183
|
|
|
184
|
+
> **Audit of 0.7.26 (2026-11-01), fixed for 0.7.27.** A seventh independent audit named two defects; both reproduced on
|
|
185
|
+
> the PyPI 0.7.26 wheel. A clause that only mentioned speed ("Speed was recorded separately; no route is longer than
|
|
186
|
+
> 26 km") let a speed count named "routes longer than 26 km" back the length claim, and the same for cargo named as
|
|
187
|
+
> passenger routes: the key's words were looked for anywhere in the statement (HG-026-01). Reproducing it found a
|
|
188
|
+
> wider gap: "no crate is heavier than 40 kg" wasn't read as an absolute, so it went in with no count or with a count
|
|
189
|
+
> of crate prices. And the 0.7.26 note that a count of the whole condition settles a mixed "and" / "or" clause promised
|
|
190
|
+
> something nothing implements (HG-026-02). A key's words must now be said where the claim states the count's figure;
|
|
191
|
+
> "no …" before any comparison is an absolute; eight more comparison pairs have axes of their own, and an unknown
|
|
192
|
+
> "-er than" is met only by a count saying the same word; the mixed-clause refusal says to split the claim. Eleven
|
|
193
|
+
> paraphrases written after the first fix found three more gaps (a three-letter subject, "span", a comma-led aside),
|
|
194
|
+
> fixed before merge. Replaying the 1,900 recorded adversarial entries changes none.
|
|
195
|
+
>
|
|
196
|
+
> **Where that stands.** #98 is merged with green CI (17 of 17 checks). This is still word matching; the typed claim
|
|
197
|
+
> layer the audits recommend is not built. Released as 0.7.27.
|
|
198
|
+
|
|
184
199
|
## Recommendation: CONDITIONAL GO
|
|
185
200
|
|
|
186
201
|
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.27"
|
|
20
20
|
|
|
21
21
|
from .bundle import anchored, verify_bundle # noqa: E402
|
|
22
22
|
from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
|
|
@@ -243,7 +243,7 @@ def _bound(s: Snap, meas: list, stmt: str = "") -> tuple[list[tuple[dict, dict]]
|
|
|
243
243
|
bad = True
|
|
244
244
|
# a name that says no comparison ("fitting selections") doesn't say another count; one that does must keep
|
|
245
245
|
# what the key counted, here or in the statement
|
|
246
|
-
unsaid = _unsaid(_subject_of(key), f"{m.get('name') or ''} . {stmt}")
|
|
246
|
+
unsaid = _unsaid(_subject_of(key), f"{m.get('name') or ''} . {_clauses_at(stmt, _count_figures(m, rv))}")
|
|
247
247
|
if not bad and unsaid and _comparisons_in(str(m.get("name") or "")):
|
|
248
248
|
problems.append(Problem("MEASUREMENT_MISMATCH", f"measurement {m.get('name')}: result {m['result']} "
|
|
249
249
|
f"counted '{m['key']}', and neither the name nor the statement says what it "
|
|
@@ -256,6 +256,32 @@ def _bound(s: Snap, meas: list, stmt: str = "") -> tuple[list[tuple[dict, dict]]
|
|
|
256
256
|
return good, problems
|
|
257
257
|
|
|
258
258
|
|
|
259
|
+
# where one thing said ends and another begins: a sentence, a semicolon or colon, or a comma before a conjunction
|
|
260
|
+
_SAYING = re.compile(r"[;:!?]|\.(?!\d)|,\s*(?=(?:and|but|while|whereas|although|though|so|yet)\b)|"
|
|
261
|
+
r"\b(?:although|though|whereas)\b", re.I)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _clauses_at(stmt: str, figs: set[float]) -> str:
|
|
265
|
+
"""The parts of ``stmt`` that state one of ``figs``: what a count against that figure must be about is said there,
|
|
266
|
+
not in another sentence. "Speed was recorded separately; no route is longer than 26 km" says nothing about speed
|
|
267
|
+
where it says 26 km, so a speed count named "routes longer than 26 km" can't borrow the word (audit HG-026-01).
|
|
268
|
+
The whole statement when no part states one."""
|
|
269
|
+
if not stmt or not figs:
|
|
270
|
+
return stmt
|
|
271
|
+
parts = [p for p in _SAYING.split(stmt) if p and p.strip()]
|
|
272
|
+
hit = [_from_claim(p) for p in parts if {round(v[0], 6) for v in numbers(p)} & {round(f, 6) for f in figs}]
|
|
273
|
+
return " . ".join(hit) if hit else stmt
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _from_claim(part: str) -> str:
|
|
277
|
+
"""A part from the comma before its absolute or extreme: "Rural sites aside, no urban clinic had waits above 45"
|
|
278
|
+
claims only "no urban clinic had waits above 45"."""
|
|
279
|
+
hit = L.ABSOLUTE.search(part) or L.SUPERLATIVE.search(part)
|
|
280
|
+
if not hit:
|
|
281
|
+
return part
|
|
282
|
+
return part[max((x.end() for x in re.finditer(r",(?!\d)", part[:hit.start()])), default=0):]
|
|
283
|
+
|
|
284
|
+
|
|
259
285
|
def _axis_words(axes: set[str]) -> str:
|
|
260
286
|
return " / ".join(sorted("/".join(w for w, (a, _s) in _DIRECTION_OF.items() if a == x and not w.endswith("est"))
|
|
261
287
|
for x in axes))
|
|
@@ -309,13 +335,11 @@ _PROVEN = re.compile(r"^prov(?:es?|en|ed)$|^proof$", re.I)
|
|
|
309
335
|
|
|
310
336
|
# the figure a count is about: "fitting selections worth more than 93", "routes shorter than 26 km" (or, matching a
|
|
311
337
|
# figure the claim names, "worth_more_93"). A count of nothing beyond 140 says nothing about 93 (audit HG-020-01/02).
|
|
312
|
-
_COUNT_FIGURE = re.compile(r"\b(?:(?:
|
|
313
|
-
r"slower|larger|smaller)\s+than|above|below|over|under|beyond|exceed\w*|beat\w*)\s+\$?"
|
|
338
|
+
_COUNT_FIGURE = re.compile(r"\b(?:(?:" + L.CMP_WORDS + r"|(?!(?:" + L.NOT_CMP + r")\b)[a-z]{3,}er)\s+than|above|below|over|under|beyond|exceed\w*|beat\w*)\s+\$?"
|
|
314
339
|
r"(-?\d[\d,]*(?:\.\d+)?)", re.I)
|
|
315
340
|
# the figure a name gives before its comparison or with "at least": "routes 26 km or longer", "routes at least 26 km"
|
|
316
341
|
_COUNT_FIGURE_BOUNDARY = re.compile(r"\bat\s+(?:least|most)\s+\$?(-?\d[\d,]*(?:\.\d+)?)|\$?(-?\d[\d,]*(?:\.\d+)?)"
|
|
317
|
-
r"(?:\s+[a-z%]+){0,2}\s+or\s+(?:
|
|
318
|
-
r"shorter|longer|cheaper|faster|slower|larger|smaller)\b", re.I)
|
|
342
|
+
r"(?:\s+[a-z%]+){0,2}\s+or\s+(?:" + L.CMP_WORDS + r")\b", re.I)
|
|
319
343
|
|
|
320
344
|
|
|
321
345
|
def _named_figure(name: str) -> tuple[float, str] | None:
|
|
@@ -329,16 +353,13 @@ def _named_figure(name: str) -> tuple[float, str] | None:
|
|
|
329
353
|
return None
|
|
330
354
|
|
|
331
355
|
|
|
332
|
-
_KEY_FIGURE = re.compile(r"\b(?:
|
|
333
|
-
r"slower|larger|smaller|above|below|over|under|beyond|exceed\w*|beat\w*)(?:\s+than)?\s+"
|
|
356
|
+
_KEY_FIGURE = re.compile(r"\b(?:" + L.CMP_WORDS + r"|above|below|over|under|beyond|exceed\w*|beat\w*)(?:\s+than)?\s+"
|
|
334
357
|
r"(\d+(?:\.\d+)?)$", re.I)
|
|
335
358
|
# the figure an absolute names in its own clause: "nothing worth more than 93 fits", "no route is shorter than 26 km"
|
|
336
|
-
_CLAIM_FIGURE = re.compile(r"\b(?:
|
|
337
|
-
r"slower|larger|smaller)\s+than\s+\$?(-?\d[\d,]*(?:\.\d+)?)|\b(?:above|below|beyond|"
|
|
359
|
+
_CLAIM_FIGURE = re.compile(r"\b(?:" + L.CMP_WORDS + r"|(?!(?:" + L.NOT_CMP + r")\b)[a-z]{3,}er)\s+than\s+\$?(-?\d[\d,]*(?:\.\d+)?)|\b(?:above|below|beyond|"
|
|
338
360
|
r"exceed(?:s|ing)?)\s+\$?(-?\d[\d,]*(?:\.\d+)?)", re.I)
|
|
339
361
|
_CLAIM_BOUNDARY = re.compile(r"\bat\s+(?:least|most)\s+\$?(-?\d[\d,]*(?:\.\d+)?)|\$?(-?\d[\d,]*(?:\.\d+)?)"
|
|
340
|
-
r"(?:\s+[a-z%]+){0,2}\s+or\s+(?:
|
|
341
|
-
r"shorter|longer|cheaper|faster|slower|larger|smaller)\b", re.I)
|
|
362
|
+
r"(?:\s+[a-z%]+){0,2}\s+or\s+(?:" + L.CMP_WORDS + r")\b", re.I)
|
|
342
363
|
_SUBCLAUSE = re.compile(r",(?!\d)|[;:!?()]|\.(?!\d)|\b(?:and|but|while|whereas|although|so)\b", re.I)
|
|
343
364
|
|
|
344
365
|
|
|
@@ -382,6 +403,14 @@ _DIRECTION_WORDS = {
|
|
|
382
403
|
"fast": {1: "faster fastest", -1: "slower slowest"},
|
|
383
404
|
"cost": {1: "dearer dearest", -1: "cheaper cheapest"},
|
|
384
405
|
"time": {1: "later latest", -1: "earlier earliest"},
|
|
406
|
+
"heavy": {1: "heavier heaviest", -1: "lighter lightest"},
|
|
407
|
+
"warm": {1: "warmer warmest hotter hottest", -1: "colder coldest cooler coolest"},
|
|
408
|
+
"old": {1: "older oldest", -1: "younger youngest newer newest"},
|
|
409
|
+
"tall": {1: "taller tallest"},
|
|
410
|
+
"wide": {1: "wider widest", -1: "narrower narrowest"},
|
|
411
|
+
"deep": {1: "deeper deepest", -1: "shallower shallowest"},
|
|
412
|
+
"loud": {1: "louder loudest", -1: "quieter quietest"},
|
|
413
|
+
"strong": {1: "stronger strongest", -1: "weaker weakest"},
|
|
385
414
|
}
|
|
386
415
|
_DIRECTION_OF = {w: (axis, sign) for axis, signs in _DIRECTION_WORDS.items() for sign, ws in signs.items()
|
|
387
416
|
for w in ws.split()}
|
|
@@ -389,8 +418,7 @@ _DIRECTION_OF = {w: (axis, sign) for axis, signs in _DIRECTION_WORDS.items() for
|
|
|
389
418
|
# not strict, so a route of exactly 26 lies inside them and outside "longer than" (audit HG-023-02). Six operators;
|
|
390
419
|
# "=" and "!=" point nowhere.
|
|
391
420
|
_OPS = {">": (1, True), ">=": (1, False), "<": (-1, True), "<=": (-1, False), "=": None, "!=": None}
|
|
392
|
-
_CMP = r"(
|
|
393
|
-
r"smaller|bigger|earlier|later)"
|
|
421
|
+
_CMP = r"(" + L.CMP_WORDS + r")"
|
|
394
422
|
_NOT_STRICT = re.compile(r"\bat\s+(least|most)\b|\b(?:not|no)\s+" + _CMP + r"\s+than\b|\bor\s+" + _CMP + r"\b", re.I)
|
|
395
423
|
|
|
396
424
|
|
|
@@ -408,7 +436,13 @@ def _comparisons_in(text: str) -> set[tuple[str, int, bool]]:
|
|
|
408
436
|
else:
|
|
409
437
|
out.add((*_DIRECTION_OF[h.group(3)], False))
|
|
410
438
|
rest = _NOT_STRICT.sub(" ", text)
|
|
411
|
-
|
|
439
|
+
# a comparative the checks don't know ("brighter than") is an axis of its own: only a count that says the same word
|
|
440
|
+
# meets it, so nothing else can back a claim made with it (audit HG-026-01)
|
|
441
|
+
unknown = {(w, 1, True) for w in _UNKNOWN_CMP.findall(rest) if w not in _DIRECTION_OF}
|
|
442
|
+
return out | unknown | {(*_DIRECTION_OF[w], True) for w in re.findall(r"[a-z]+", rest) if w in _DIRECTION_OF}
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
_UNKNOWN_CMP = re.compile(r"\b(?!(?:" + L.NOT_CMP + r")\b)([a-z]{3,}er)\s+than\b")
|
|
412
446
|
|
|
413
447
|
|
|
414
448
|
def _directions(text: str) -> set[tuple[str, int]]:
|
|
@@ -582,12 +616,23 @@ _COUNTING = set("count counts counted counting number numbers total totals tally
|
|
|
582
616
|
_HOW_MANY = set("nothing none never nobody every always ever each whole entire".split())
|
|
583
617
|
|
|
584
618
|
|
|
619
|
+
# three-letter words that say nothing about what was counted
|
|
620
|
+
_SHORT_SKIP = set("has had did got get let may can one two six ten few new old top max min own via nor yet per".split())
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def _stems(text: str) -> set[str]:
|
|
624
|
+
"""``L.content_words`` and the three-letter words it leaves out, which can name what was counted: a count of
|
|
625
|
+
"bridge age above 60" is of ages, and "age" must be said for it to back a claim (audit HG-026-01)."""
|
|
626
|
+
return L.content_words(text) | {w for w in re.findall(r"[a-z]+", (text or "").lower())
|
|
627
|
+
if len(w) == 3 and w not in L.STOP and w not in _SHORT_SKIP}
|
|
628
|
+
|
|
629
|
+
|
|
585
630
|
def _modifiers(words: list[str], i: int) -> set[str]:
|
|
586
631
|
"""The content words right before ``words[i]``, up to a determiner, number, preposition or comparison: "no passenger
|
|
587
632
|
route" gives {passeng}, "the 3 cargo routes" {cargo}, "the shortest route" nothing."""
|
|
588
633
|
out: set[str] = set()
|
|
589
634
|
for w in reversed(words[:i]):
|
|
590
|
-
cw =
|
|
635
|
+
cw = _stems(w)
|
|
591
636
|
if not cw or w in _DETERMINERS or w in _PREPOSITIONS or w in _DIRECTION_OF or w in _COUNTING | _HOW_MANY:
|
|
592
637
|
break
|
|
593
638
|
out |= cw
|
|
@@ -616,7 +661,7 @@ def _subject_parts(text: str) -> tuple[set[str], set[str]]:
|
|
|
616
661
|
while i > 1 and words[i - 1] in _MEASURED_BY: # "selections worth more": the head is selections
|
|
617
662
|
whole -= L.content_words(words[i - 1])
|
|
618
663
|
i -= 1
|
|
619
|
-
head =
|
|
664
|
+
head = _stems(words[i - 1]) & whole
|
|
620
665
|
return head, whole - head
|
|
621
666
|
|
|
622
667
|
|
|
@@ -626,15 +671,18 @@ _MEASURED_BY = set("worth valued priced weighing costing lasting taking".split()
|
|
|
626
671
|
|
|
627
672
|
_COPULAS = set("is are was were be been being".split())
|
|
628
673
|
# the property a comparison word compares: a count of "route length above 26" is said by "no route is longer than 26"
|
|
629
|
-
_PROPERTY_AXIS = {s: axis for axis, ws in {"long": "length distance", "fast": "speed velocity pace",
|
|
630
|
-
"cost": "cost price", "time": "date time"
|
|
631
|
-
|
|
674
|
+
_PROPERTY_AXIS = {s: axis for axis, ws in {"long": "length distance span", "fast": "speed velocity pace",
|
|
675
|
+
"cost": "cost price", "time": "date time", "heavy": "weight mass",
|
|
676
|
+
"warm": "temperature heat", "old": "age", "tall": "height",
|
|
677
|
+
"wide": "width breadth", "deep": "depth", "loud": "loudness",
|
|
678
|
+
"strong": "strength"}.items()
|
|
679
|
+
for w in ws.split() for s in _stems(w)}
|
|
632
680
|
|
|
633
681
|
|
|
634
682
|
def _unsaid(stems: set[str], text: str) -> set[str]:
|
|
635
683
|
"""``stems`` that ``text`` doesn't say: neither as words nor, for a property, by comparing along its axis."""
|
|
636
684
|
axes = {c[0] for c in _comparisons_in(text) if c[0]}
|
|
637
|
-
return {s for s in stems -
|
|
685
|
+
return {s for s in stems - _stems(text) if _PROPERTY_AXIS.get(s) not in axes}
|
|
638
686
|
|
|
639
687
|
|
|
640
688
|
def _subjects_at(stmt: str, axes: set[str]) -> set[str]:
|
|
@@ -733,9 +781,10 @@ def _absolutes(stmt: str, scope: str, body: dict, bound: list) -> list[Problem]:
|
|
|
733
781
|
seen.add((tuple(group), _ALL.match(w) is None))
|
|
734
782
|
if group[0] is MIXED:
|
|
735
783
|
p = Problem("ABSOLUTE_WITHOUT_COUNT", f"'{w}' rules on comparisons joined by both 'and' and 'or': "
|
|
736
|
-
"counts of each comparison on its own can't settle that either way
|
|
737
|
-
"whole condition
|
|
738
|
-
|
|
784
|
+
"counts of each comparison on its own can't settle that either way, and no count can be "
|
|
785
|
+
"tied to the whole condition. Split it into claims that each join their comparisons one "
|
|
786
|
+
"way (audit HG-026-02)",
|
|
787
|
+
fix="split the claim into clauses that each join their comparisons one way", words=words)
|
|
739
788
|
if p.text() not in {q.text() for q in out}:
|
|
740
789
|
out.append(p)
|
|
741
790
|
continue
|
|
@@ -860,8 +909,7 @@ def _denied(after: str) -> bool:
|
|
|
860
909
|
|
|
861
910
|
# a count named for what lies beyond a figure ("fitting selections worth more", "shorter routes connecting") is about
|
|
862
911
|
# any extreme the statement claims, whatever words the statement uses for it ("the highest total value is 99")
|
|
863
|
-
_BEYOND = re.compile(r"\b(?:
|
|
864
|
-
r"larger|smaller|above|below|exceed\w*|beat\w*)\b", re.I)
|
|
912
|
+
_BEYOND = re.compile(r"\b(?:" + L.CMP_WORDS + r"|above|below|exceed\w*|beat\w*)\b", re.I)
|
|
865
913
|
|
|
866
914
|
|
|
867
915
|
def _extremes(stmt: str) -> list[str]:
|
|
@@ -14,6 +14,12 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
import re
|
|
16
16
|
|
|
17
|
+
# every comparative the checks know, and the "-er than" words that aren't one ("other than", "rather than")
|
|
18
|
+
CMP_WORDS = ("longer|shorter|more|less|fewer|greater|higher|lower|larger|smaller|bigger|better|worse|faster|slower|"
|
|
19
|
+
"cheaper|dearer|earlier|later|heavier|lighter|warmer|hotter|colder|cooler|older|younger|newer|taller|wider|"
|
|
20
|
+
"narrower|deeper|shallower|louder|quieter|stronger|weaker")
|
|
21
|
+
NOT_CMP = "other|rather|either|neither|whether|never|ever|over|under|after|per|number|order|matter"
|
|
22
|
+
|
|
17
23
|
ABSOLUTE = re.compile(
|
|
18
24
|
r"\b(never|always|all|every|none|nothing|no one|nobody|impossible|cannot|can't|proves?|proven|proof|certainly|"
|
|
19
25
|
r"certain|guaranteed?|guarantees|eliminat(?:es|ed|ing)|entirely|completely|totally|universally|invariably|"
|
|
@@ -22,14 +28,16 @@ ABSOLUTE = re.compile(
|
|
|
22
28
|
# "no route is longer than 26 km": a universal negative comparison, the same claim as "nothing is longer"; only
|
|
23
29
|
# where "no" opens its clause ("routes that visit no town twice" is not one); "no route is 26 km or longer" and
|
|
24
30
|
# "no route is at least 26 km" too
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
r"
|
|
28
|
-
r"
|
|
31
|
+
# Any comparison word, and any verb before it: "no crate is heavier than 40 kg", "no shift had errors above 5",
|
|
32
|
+
# "no crate weighs more than 40 kg" (audit HG-026-01: these went unread, so any count backed them)
|
|
33
|
+
r"|(?<![a-z] )no(?=\s+(?:[a-z]+\s+){1,4}?(?:(?:is|are|was|were)\s+(?:any\s+)?)?(?:at\s+(?:least|most)\s|"
|
|
34
|
+
r"\$?\d[\d,.]*(?:\s+[a-z%]+){0,2}\s+or\s+(?:" + CMP_WORDS + r")\b|(?:" + CMP_WORDS + r"|above|below|over|under|"
|
|
35
|
+
r"exceed(?:s|ed|ing)?)\b|(?!(?:" + NOT_CMP + r")\b)[a-z]{3,}er\s+than\b)))\b|(?<![\d.])100\s*%", re.I)
|
|
29
36
|
# a superlative names an extreme ("the highest value is 93"): a count of anything beyond it contradicts it
|
|
30
37
|
SUPERLATIVE = re.compile(
|
|
31
38
|
r"\b(highest|lowest|largest|smallest|shortest|longest|greatest|fewest|best|worst|maximum|minimum|optimal|"
|
|
32
|
-
r"optimum|cheapest|fastest|slowest|biggest|earliest|latest
|
|
39
|
+
r"optimum|cheapest|fastest|slowest|biggest|earliest|latest|heaviest|lightest|warmest|hottest|coldest|coolest|"
|
|
40
|
+
r"oldest|youngest|newest|tallest|widest|narrowest|deepest|shallowest|loudest|quietest|strongest|weakest)\b", re.I)
|
|
33
41
|
COMPARATIVE = re.compile(
|
|
34
42
|
r"\b(better|worse|beats?|beat|outperform(?:s|ed|ing)?|underperform(?:s|ed|ing)?|improv(?:es|ed|ement|ing)|"
|
|
35
43
|
r"superior|inferior|exceeds?|exceeded|higher than|lower than|more than|fewer than|less than|faster|slower|"
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""A word said somewhere else in the statement, and a comparison the checks didn't know (audit of 0.7.26, HG-026-01
|
|
2
|
+
and -02).
|
|
3
|
+
|
|
4
|
+
A count's key says what was counted; the claimant's name for it must keep that, here or where the claim states the
|
|
5
|
+
count's figure. "Speed was recorded separately; no route is longer than 26 km" says nothing about speed where it says
|
|
6
|
+
26 km, so a speed count named "routes longer than 26 km" can't borrow the word. The invariant: an extra clause never
|
|
7
|
+
turns a refusal into an acceptance, and an honest count backs the claim with or without it. "No crate is heavier than
|
|
8
|
+
40 kg" is an absolute like "no route is longer than 26 km", and a comparison the checks don't know ("brighter than")
|
|
9
|
+
is met only by a count that says the same word."""
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from hypgraph import Graph, GraphError, Policy
|
|
13
|
+
|
|
14
|
+
SCOPE = "The surveyed items, against the stated mark."
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def outcome(values, statement, meas=()):
|
|
18
|
+
g = Graph(policy=Policy(reviewers={"critic"}))
|
|
19
|
+
inv = g.author("curator").investigation("Survey", "What do the surveyed items show?")["id"]
|
|
20
|
+
lab = g.author("lab")
|
|
21
|
+
run = lab.add("run", inv, kind="lab", state="succeeded", name="Measured the surveyed items.")
|
|
22
|
+
r = lab.add("result", inv, kind="computed", outcome="positive", summary="Counts of the surveyed items.",
|
|
23
|
+
values=values, links=[{"rel": "produced", "from": run["id"]}])["id"]
|
|
24
|
+
try:
|
|
25
|
+
g.author("a").add("claim", inv, statement=statement, scope=SCOPE,
|
|
26
|
+
measurements=[{**m, "result": r} for m in meas],
|
|
27
|
+
links=[{"rel": "supports", "from": r, "why": "Cited for the claim."}])
|
|
28
|
+
return []
|
|
29
|
+
except GraphError as e:
|
|
30
|
+
return [p["code"] for p in e.problems]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def t(k, figure, op=">", n=3):
|
|
34
|
+
return {"k": k, "n": n, "figure": figure, "op": op}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def M(key, name, k, n=3):
|
|
38
|
+
return {"name": name, "key": key, "k": k, "n": n}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# (frozen values, the relabelled count, the claim, the claim with the borrowed word said elsewhere)
|
|
42
|
+
LAUNDER = {
|
|
43
|
+
"speed as length": ({"route_speed_above_26": t(0, 26)}, M("route_speed_above_26", "routes longer than 26 km", 0),
|
|
44
|
+
"No route is longer than 26 km among the 3 routes.",
|
|
45
|
+
["Speed was recorded separately; no route is longer than 26 km among the 3 routes."]),
|
|
46
|
+
"cargo as passenger": ({"cargo_route_longer_26": t(0, 26), "passenger_route_longer_26": t(1, 26)},
|
|
47
|
+
M("cargo_route_longer_26", "passenger route longer than 26 km", 0),
|
|
48
|
+
"No passenger route is longer than 26 km among the 3 routes.",
|
|
49
|
+
["Cargo routes were inspected; no passenger route is longer than 26 km among the 3 "
|
|
50
|
+
"routes."]),
|
|
51
|
+
"night as day": ({"night_shift_errors_above_5": t(0, 5), "day_shift_errors_above_5": t(3, 5)},
|
|
52
|
+
M("night_shift_errors_above_5", "day shift errors above 5", 0),
|
|
53
|
+
"No day shift had errors above 5 among the 3 shifts.",
|
|
54
|
+
["The night shift was audited separately; no day shift had errors above 5 among the 3 shifts."]),
|
|
55
|
+
"price as weight": ({"crate_price_above_40": t(0, 40)}, M("crate_price_above_40", "crates heavier than 40 kg", 0),
|
|
56
|
+
"No crate is heavier than 40 kg among the 3 crates.",
|
|
57
|
+
["Prices were logged too; no crate is heavier than 40 kg among the 3 crates."]),
|
|
58
|
+
"frozen as fresh": ({"frozen_sample_warmer_8": t(0, 8), "fresh_sample_warmer_8": t(2, 8)},
|
|
59
|
+
M("frozen_sample_warmer_8", "fresh sample warmer than 8 C", 0),
|
|
60
|
+
"No fresh sample is warmer than 8 C among the 3 samples.",
|
|
61
|
+
["Frozen samples were checked as well; no fresh sample is warmer than 8 C among the 3 "
|
|
62
|
+
"samples."]),
|
|
63
|
+
"age as span": ({"bridge_age_above_60": t(0, 60)}, M("bridge_age_above_60", "bridges longer than 60 m", 0),
|
|
64
|
+
"No bridge is longer than 60 m among the 3 bridges.",
|
|
65
|
+
["Ages were taken from the register. No bridge is longer than 60 m among the 3 bridges.",
|
|
66
|
+
"Although age was recorded, no bridge is longer than 60 m among the 3 bridges.",
|
|
67
|
+
"We noted each bridge's age, and no bridge is longer than 60 m among the 3 bridges."]),
|
|
68
|
+
"rural as urban": ({"rural_clinic_waits_above_45": t(0, 45), "urban_clinic_waits_above_45": t(3, 45)},
|
|
69
|
+
M("rural_clinic_waits_above_45", "urban clinic waits above 45 minutes", 0),
|
|
70
|
+
"No urban clinic had waits above 45 minutes among the 3 clinics.",
|
|
71
|
+
["Rural clinics were visited first; no urban clinic had waits above 45 minutes among the 3 "
|
|
72
|
+
"clinics.",
|
|
73
|
+
"Rural sites aside, no urban clinic had waits above 45 minutes among the 3 clinics."]),
|
|
74
|
+
"unknown comparison": ({"lamp_wattage_above_40": t(0, 40)}, M("lamp_wattage_above_40", "lamps brighter than 40 lux",
|
|
75
|
+
0),
|
|
76
|
+
"No lamp is brighter than 40 lux among the 3 lamps.",
|
|
77
|
+
["Wattage was read off each label. No lamp is brighter than 40 lux among the 3 lamps."]),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@pytest.mark.parametrize("case", list(LAUNDER))
|
|
82
|
+
def test_a_word_said_elsewhere_does_not_launder_a_count(case):
|
|
83
|
+
"""HG-026-01: the missing word was looked for anywhere in the statement, so a clause that only mentioned speed (or
|
|
84
|
+
cargo, or the night shift) let a count of it back a claim about something else."""
|
|
85
|
+
values, m, claim, padded = LAUNDER[case]
|
|
86
|
+
assert "MEASUREMENT_MISMATCH" in outcome(values, claim, [m])
|
|
87
|
+
for s in padded:
|
|
88
|
+
assert "MEASUREMENT_MISMATCH" in outcome(values, s, [m]), s
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
HONEST = [
|
|
92
|
+
({"passenger_route_longer_26": t(0, 26)}, M("passenger_route_longer_26", "passenger routes longer than 26 km", 0),
|
|
93
|
+
["No passenger route is longer than 26 km among the 3 routes.",
|
|
94
|
+
"Cargo routes were inspected; no passenger route is longer than 26 km among the 3 routes."]),
|
|
95
|
+
({"bridge_span_above_60": t(0, 60)}, M("bridge_span_above_60", "bridge spans above 60 m", 0),
|
|
96
|
+
["No bridge is longer than 60 m among the 3 bridges.",
|
|
97
|
+
"Ages were taken from the register. No bridge is longer than 60 m among the 3 bridges."]),
|
|
98
|
+
({"well_temperature_above_30": t(0, 30)}, M("well_temperature_above_30", "well temperature above 30 C", 0),
|
|
99
|
+
["No well is warmer than 30 C among the 3 wells.",
|
|
100
|
+
"Depth was logged: no well is warmer than 30 C among the 3 wells."]),
|
|
101
|
+
({"crate_weight_above_40": t(0, 40)}, M("crate_weight_above_40", "crates heavier than 40 kg", 0),
|
|
102
|
+
["No crate is heavier than 40 kg among the 3 crates.",
|
|
103
|
+
"Prices were logged too; no crate is heavier than 40 kg among the 3 crates."]),
|
|
104
|
+
({"lamps_brighter_than_40": t(0, 40)}, M("lamps_brighter_than_40", "lamps brighter than 40 lux", 0),
|
|
105
|
+
["No lamp is brighter than 40 lux among the 3 lamps."]),
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@pytest.mark.parametrize("i", range(len(HONEST)))
|
|
110
|
+
def test_an_honest_count_backs_the_claim_with_or_without_another_clause(i):
|
|
111
|
+
values, m, statements = HONEST[i]
|
|
112
|
+
for s in statements:
|
|
113
|
+
assert outcome(values, s, [m]) == [], s
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@pytest.mark.parametrize("s", ["No crate is heavier than 40 kg among the 3 crates.",
|
|
117
|
+
"No lamp is brighter than 40 lux among the 3 lamps.",
|
|
118
|
+
"No shift had errors above 5 among the 3 shifts."])
|
|
119
|
+
def test_an_absolute_with_any_comparison_needs_a_count(s):
|
|
120
|
+
"""These weren't read as absolutes, so the record took them with no count at all, or with a count of anything."""
|
|
121
|
+
assert "ABSOLUTE_WITHOUT_COUNT" in outcome({"other": t(0, 1)}, s)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_a_count_cannot_be_tied_to_a_mixed_condition():
|
|
125
|
+
"""HG-026-02: 0.7.26's notes said a count of the whole condition would settle a mixed "and" / "or" clause; nothing
|
|
126
|
+
ties a count to it, so the refusal says to split the claim."""
|
|
127
|
+
values = {"longer_10_and_shorter_30_or_shorter_6": t(3, 10), "longer_10": t(2, 10)}
|
|
128
|
+
codes = outcome(values, "Every route is longer than 10 km and shorter than 30 km or shorter than 6 km among the 3 "
|
|
129
|
+
"routes.", [M("longer_10", "routes longer than 10 km", 2)])
|
|
130
|
+
assert "ABSOLUTE_WITHOUT_COUNT" in codes
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|