hypgraph 0.7.24__tar.gz → 0.7.25__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.24 → hypgraph-0.7.25}/CHANGELOG.md +25 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/PKG-INFO +1 -1
- {hypgraph-0.7.24 → hypgraph-0.7.25}/docs/PLAN.md +1 -1
- {hypgraph-0.7.24 → hypgraph-0.7.25}/docs/RELEASE-READINESS.md +17 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/__init__.py +1 -1
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/checks.py +119 -19
- hypgraph-0.7.25/tests/test_counts_that_bear.py +160 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/.gitignore +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/AGENTS.md +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/CITATION.cff +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/DISCLAIMER.md +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/LICENSE +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/NOTICE +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/README.md +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/docs/decisions/ADR-002-governance-profiles.md +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/docs/profile-workflows-plan.md +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/docs/review-2026-10-24.md +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/docs/site-plan.md +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/docs/spec.md +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/docs/ui-plan.md +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/docs/verification.md +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/docs/workflows/README.md +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/docs/workflows/knowledge.md +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/docs/workflows/optimization.md +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/docs/workflows/scientific.md +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/docs/workflows/software.md +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/docs/workflows/trading.md +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/examples/frameworks/README.md +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/pyproject.toml +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/__main__.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/blind.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/bundle.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/cli.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/coord.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/demos/__init__.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/demos/broad_street.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/demos/linked.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/derive.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/graph.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/language.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/mcp.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/plan.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/policy.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/profiles.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/references.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/refusals.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/report_check.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/schema.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/server.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/skill/SKILL.md +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/stats.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/store.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/ui/__init__.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/ui/atlas.css +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/ui/atlas.js +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/ui/hypgraph.css +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/ui/hypgraph.i18n.js +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/ui/hypgraph.js +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/ui/hypgraph.svg +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/ui/index.html +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/ui/vendor/cose-base.js +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/ui/vendor/layout-base.js +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/src/hypgraph/workflow.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_accept_work_kind.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_anchoring.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_blind.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_blind_oracles.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_bound_support.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_bundle_telemetry.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_cancel_releases_lease.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_cited_and_failed_support.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_contradictions.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_coord.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_d10.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_direction_and_units.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_engine.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_engine_fixes.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_framework_examples.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_i18n.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_incremental.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_link_translate.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_linked_records.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_log_and_check.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_measure_questions.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_opposed_answers.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_option_lead.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_order_detail.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_profiles.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_profiles_041.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_quantifiers_and_identity.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_questions_not_assertions.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_references.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_refusal_friction.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_refusal_results.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_refusals.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_relation_words.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_report_answers.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_rerunnable.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_restates_scale.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_server_auth.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_server_hosts.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_shape_refusals.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_stats_not_backtests.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_stats_values.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_surfaces.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_telemetry_stream.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_trust_boundary.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_ui_app.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_ui_engine.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_uncorrected_effect.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_unnamed_inputs.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_verify_complete.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_verify_lifecycle.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_verify_scope.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_verify_scoped_views.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_verify_trust.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_verify_views.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_versions_not_figures.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_vocabulary_friction.py +0 -0
- {hypgraph-0.7.24 → hypgraph-0.7.25}/tests/test_workflows.py +0 -0
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.25 — 2026-11-01: which counts bear on a claim, joined comparisons, boundaries in names
|
|
4
|
+
|
|
5
|
+
Fixes from an independent audit of 0.7.24 (HG-024-01 to -04); each case below failed on 0.7.24 and is now a test
|
|
6
|
+
(`tests/test_counts_that_bear.py`).
|
|
7
|
+
|
|
8
|
+
- **A count with no comparison doesn't back one** (HG-024-01). With no comparison of its own, a count of none or all
|
|
9
|
+
was read the old way, so "3 of 3 routes processed" backed "nothing is longer than 26 km" and "every route is longer
|
|
10
|
+
than 26 km". A count that states no comparison now doesn't bear on a claim that rules one out
|
|
11
|
+
(`ABSOLUTE_WITHOUT_COUNT`). A claim with no comparison ("no route ever failed") reads a plain count as before.
|
|
12
|
+
- **A count of one kind doesn't back a claim about another** (HG-024-02). A count and a claim were matched on any shared
|
|
13
|
+
word, so none of 3 cargo routes longer than 26 km backed "no passenger route is longer than 26 km" while the passenger
|
|
14
|
+
count was 1 of 3, and backed "no route is longer" too. A count names what it counts in the plural ("cargo routes");
|
|
15
|
+
where the statement uses that word, the words right before it say which kind. A count backs a claim only if every
|
|
16
|
+
kind it names is one the statement names; a count of one kind that finds some still refutes a claim about all, and
|
|
17
|
+
two kinds named apart (cargo, passenger) are left unresolved.
|
|
18
|
+
- **Two comparisons joined in one clause** (HG-024-03). "And" ended the clause, so "no route is shorter than 20 km and
|
|
19
|
+
longer than 30 km" was refused by a route shorter than 20, and "every route is longer than 10 km and shorter than 30
|
|
20
|
+
km" went in with "shorter than 30" unread, even with the count that refutes it bound. An "and" or "or" before a
|
|
21
|
+
comparison with no subject of its own now keeps it in the clause. "No … A or B" and "every … A and B" are false if
|
|
22
|
+
either part fails; "no … A and B" and "every … A or B" rule out only what meets both, so none of one part backs them
|
|
23
|
+
and a count of each part on its own leaves them unresolved.
|
|
24
|
+
- **A count's name must say its boundary** (HG-024-04). Only a name pointing the other way was caught: a `>=` count
|
|
25
|
+
named "routes longer than 26 km", or a `>` count named "26 km or longer", went in. A name that leaves the figure out
|
|
26
|
+
where the count took it in, or the other way round, is now `MEASUREMENT_MISMATCH`.
|
|
27
|
+
|
|
3
28
|
## 0.7.24 — 2026-11-01: what a quantifier needs, where a comparison's boundary falls, one check per extreme
|
|
4
29
|
|
|
5
30
|
Fixes from an independent audit of 0.7.23 (HG-023-01 to -05); each case below failed on 0.7.23 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.25
|
|
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.25 (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
|
|
@@ -149,6 +149,23 @@ was taken from a changelog without being checked against the code.*
|
|
|
149
149
|
> claim layer (subject, property, comparator, threshold, unit, quantifier) so that strong verdicts rest on typed fields
|
|
150
150
|
> rather than prose; that is not built. Released as 0.7.24.
|
|
151
151
|
|
|
152
|
+
> **Audit of 0.7.24 (2026-11-01), fixed for 0.7.25.** A fifth independent audit named four candidate defects; each
|
|
153
|
+
> reproduced on the PyPI 0.7.24 wheel, plus four of the same kind. A count with no comparison of its own ("3 of 3
|
|
154
|
+
> routes processed") backed "nothing is longer than 26 km" and "every route is longer" (HG-024-01). A count of cargo
|
|
155
|
+
> routes backed a claim about passenger routes, and about every route, because they shared the word "route"
|
|
156
|
+
> (HG-024-02). "And" ended a clause, so a true "no route is shorter than 20 km and longer than 30 km" was refused,
|
|
157
|
+
> and "every route is longer than 10 km and shorter than 30 km" went in with its second half unread, even beside the
|
|
158
|
+
> count that refutes it (HG-024-03). A `>=` count named "longer than 26 km" went in (HG-024-04). A count that states no
|
|
159
|
+
> comparison no longer bears on one; a count backs only a claim whose kinds it names, and kinds named apart are
|
|
160
|
+
> unresolved; a comparison joined by "and" / "or" stays in its clause and is read as both or either; a name must say
|
|
161
|
+
> the boundary its count used. Each case is a regression test written to fail on 0.7.24. Replaying the 1,900 recorded
|
|
162
|
+
> adversarial entries changes 7: one honest correction is no longer refused, and one correction that got in only
|
|
163
|
+
> because an unrelated zero count backed it (the HG-024-01 case) is now refused; none of the entries tries these cases.
|
|
164
|
+
>
|
|
165
|
+
> **Where that stands.** #91 is merged with green CI (17 of 17 checks); from the 0.7.25 wheel in a fresh venv the
|
|
166
|
+
> suite is 574 passed, and hypswarm (118 passed) and hypmesh (37 passed) pass on it. Kinds are read from a plural noun
|
|
167
|
+
> and the words before it, a heuristic; the typed claim layer the audits recommend is not built. Released as 0.7.25.
|
|
168
|
+
|
|
152
169
|
## Recommendation: CONDITIONAL GO
|
|
153
170
|
|
|
154
171
|
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.25"
|
|
20
20
|
|
|
21
21
|
from .bundle import anchored, verify_bundle # noqa: E402
|
|
22
22
|
from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
|
|
@@ -219,6 +219,15 @@ def _bound(s: Snap, meas: list) -> tuple[list[tuple[dict, dict]], list[Problem]]
|
|
|
219
219
|
f"way from what result {m['result']} counted for '{m['key']}' ({op})",
|
|
220
220
|
fix=f"name the count after the comparison it made ({op})"))
|
|
221
221
|
bad = True
|
|
222
|
+
# "longer than 26" leaves 26 out, "at least 26" and "26 or longer" take it in: a name that says the other
|
|
223
|
+
# boundary from the one counted says a different count (audit HG-024-04)
|
|
224
|
+
if not bad and _OPS.get(op) and any(c and not _opposed(c, ("", _OPS[op][0])) and c[2] != _OPS[op][1]
|
|
225
|
+
for _f, c in _clause_comparisons(str(m.get("name") or ""))):
|
|
226
|
+
problems.append(Problem("MEASUREMENT_MISMATCH", f"measurement {m.get('name')}: its name "
|
|
227
|
+
f"{'leaves out' if _OPS[op][1] else 'takes in'} the figure itself, but result "
|
|
228
|
+
f"{m['result']} counted '{m['key']}' with {op}",
|
|
229
|
+
fix=f"name the count after the comparison it made ({op})"))
|
|
230
|
+
bad = True
|
|
222
231
|
if not bad:
|
|
223
232
|
good.append((m, rv))
|
|
224
233
|
return good, problems
|
|
@@ -430,19 +439,50 @@ def _clause_comparisons(clause: str) -> list[tuple[float, tuple[str, int, bool]
|
|
|
430
439
|
return out
|
|
431
440
|
|
|
432
441
|
|
|
433
|
-
|
|
442
|
+
# "and" or "or" before a comparison with no subject of its own: "shorter than 20 km and longer than 30 km"
|
|
443
|
+
_JOINED = re.compile(r"\s+(?:also\s+)?(?:(?:not|no)\s+)?(?:" + _CMP + r"\s+than\b|at\s+(?:least|most)\b|above\b|below\b|"
|
|
444
|
+
r"beyond\b|exceed)", re.I)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _clause_end(stmt: str, start: int) -> int:
|
|
448
|
+
"""Where the clause that starts at ``start`` ends. An "and" that joins a second comparison to the first ("no route
|
|
449
|
+
is shorter than 20 km and longer than 30 km") doesn't end it: the two say one thing (audit HG-024-03)."""
|
|
450
|
+
for x in _SUBCLAUSE.finditer(stmt, start):
|
|
451
|
+
if not (x.group(0).lower() == "and" and _JOINED.match(stmt, x.end())):
|
|
452
|
+
return x.start()
|
|
453
|
+
return len(stmt)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _target_groups(stmt: str, w: str) -> list[list[tuple[frozenset, frozenset]]]:
|
|
434
457
|
"""What an absolute rules out, as (figures, comparisons of the exceptions), one per thing it is about. A comparison
|
|
435
458
|
in its own clause: "nothing longer than 26 km" rules out longer than 26, and "every route is longer than 26 km"
|
|
436
459
|
rules out 26 or shorter (audit HG-023-01). Not "every" in a clause that states its own count ("a count of every
|
|
437
460
|
route finds 1 of 76 shorter than 25 km"): that names the set counted, not a claim about all of it. Else each
|
|
438
|
-
extreme the statement claims, on its own (HG-023-03).
|
|
439
|
-
|
|
461
|
+
extreme the statement claims, on its own (HG-023-03).
|
|
462
|
+
|
|
463
|
+
Each group is ruled out together. Two comparisons joined in one clause are two groups where either exception
|
|
464
|
+
falsifies the claim ("no route is shorter than 20 km or longer than 30 km"; "every route is longer than 10 km and
|
|
465
|
+
shorter than 30 km"), and one group where only an exception to both does ("no route is shorter than 20 km and
|
|
466
|
+
longer than 30 km" rules out a route that is both; "every route is shorter than 20 km or longer than 30 km" one that
|
|
467
|
+
is neither) (audit HG-024-03)."""
|
|
468
|
+
out: list[list[tuple[frozenset, frozenset]]] = []
|
|
440
469
|
every = bool(_ALL.match(w)) and not _names_stated_count(stmt, w)
|
|
441
470
|
for m in re.finditer(r"\b" + re.escape(w) + r"\b", stmt, re.I):
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
471
|
+
clause = stmt[m.end():_clause_end(stmt, m.end())]
|
|
472
|
+
targets = [(frozenset({fig}), frozenset() if c is None else frozenset({_negated(c) if every else c}))
|
|
473
|
+
for fig, c in _clause_comparisons(clause)]
|
|
474
|
+
ands = any(_JOINED.match(clause, x.end()) for x in re.finditer(r"\band\b", clause, re.I))
|
|
475
|
+
ors = any(_JOINED.match(clause, x.end()) for x in re.finditer(r"\bor\b", clause, re.I))
|
|
476
|
+
if len(targets) > 1 and ands != ors and (ands != every):
|
|
477
|
+
out.append(targets)
|
|
478
|
+
else:
|
|
479
|
+
out += [[x] for x in targets]
|
|
480
|
+
return out or [[x] for x in _extreme_targets(stmt) or [(frozenset(), frozenset())]]
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _absolute_targets(stmt: str, w: str) -> list[tuple[frozenset, frozenset]]:
|
|
484
|
+
"""Every thing an absolute rules out, each on its own (``_target_groups`` says which go together)."""
|
|
485
|
+
return [x for group in _target_groups(stmt, w) for x in group]
|
|
446
486
|
|
|
447
487
|
|
|
448
488
|
def _claim_directions(stmt: str, w: str = "") -> set[tuple[str, int]]:
|
|
@@ -451,19 +491,21 @@ def _claim_directions(stmt: str, w: str = "") -> set[tuple[str, int]]:
|
|
|
451
491
|
return {(axis, sign) for _f, comps in targets for axis, sign, _strict in comps}
|
|
452
492
|
|
|
453
493
|
|
|
454
|
-
def _role(m: dict, rv: dict, a: tuple[int, int], figs: set[float], comps: set) -> str | None:
|
|
494
|
+
def _role(m: dict, rv: dict, a: tuple[int, int], figs: set[float], comps: set, stmt: str = "") -> str | None:
|
|
455
495
|
"""What a bound count does to a claim that rules out ``comps`` at ``figs``: 'back' it (none of the exceptions
|
|
456
496
|
found), 'refute' it (some found), or nothing. A count of what lies outside the exceptions is read as its
|
|
457
497
|
complement: none of 3 routes longer than 26 is 3 of 3 at 26 or shorter. A count backs only if what it counted
|
|
458
498
|
covers the exceptions, and refutes only if every one it found is one: two routes at least 26 km don't refute "no
|
|
459
499
|
route is longer than 26", nor does none longer than 26 back "no route is 26 or longer" (audit HG-023-02). Where the
|
|
460
|
-
name and key could say different comparisons, a label can widen what a count refutes, never what it backs. A
|
|
461
|
-
|
|
500
|
+
name and key could say different comparisons, a label can widen what a count refutes, never what it backs. A count
|
|
501
|
+
that says no comparison doesn't bear on a claim that rules one out: 3 of 3 routes processed says nothing about
|
|
502
|
+
routes longer than 26 km (audit HG-024-01). A claim whose comparison can't be told is read as before: none or all
|
|
503
|
+
backs, some refutes. With ``stmt``, the count must also be about what the claim is (``_subject_fit``)."""
|
|
462
504
|
k, n = a
|
|
463
505
|
cs = _count_comparisons(m, rv)
|
|
464
|
-
if
|
|
506
|
+
if comps and not cs:
|
|
465
507
|
return None
|
|
466
|
-
if not
|
|
508
|
+
if not comps:
|
|
467
509
|
role = "back" if k in (0, n) else "refute"
|
|
468
510
|
else:
|
|
469
511
|
roles = set()
|
|
@@ -484,9 +526,59 @@ def _role(m: dict, rv: dict, a: tuple[int, int], figs: set[float], comps: set) -
|
|
|
484
526
|
c = _count_figures(m, rv, figs)
|
|
485
527
|
if c and figs and not (c & figs if role == "refute" else c <= figs):
|
|
486
528
|
return None
|
|
529
|
+
if stmt:
|
|
530
|
+
covers, apart = _subject_fit(stmt, m)
|
|
531
|
+
if (role == "back" and not covers) or (role == "refute" and apart):
|
|
532
|
+
return None
|
|
487
533
|
return role
|
|
488
534
|
|
|
489
535
|
|
|
536
|
+
# words a count's name may use for the counting itself, not for what it counts
|
|
537
|
+
_COUNTING = set("count counts counted counting number numbers total totals tally share fraction rate found measured "
|
|
538
|
+
"checked matching".split())
|
|
539
|
+
# and words that say how many, not which kind: "nothing worth more", "every route"
|
|
540
|
+
_HOW_MANY = set("nothing none never nobody every always ever each whole entire".split())
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _modifiers(words: list[str], i: int) -> set[str]:
|
|
544
|
+
"""The content words right before ``words[i]``, up to a determiner, number, preposition or comparison: "no passenger
|
|
545
|
+
route" gives {passeng}, "the 3 cargo routes" {cargo}, "the shortest route" nothing."""
|
|
546
|
+
out: set[str] = set()
|
|
547
|
+
for w in reversed(words[:i]):
|
|
548
|
+
cw = L.content_words(w)
|
|
549
|
+
if not cw or w in _DETERMINERS or w in _PREPOSITIONS or w in _DIRECTION_OF or w in _COUNTING | _HOW_MANY:
|
|
550
|
+
break
|
|
551
|
+
out |= cw
|
|
552
|
+
return out
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _subject_fit(stmt: str, m: dict) -> tuple[bool, bool]:
|
|
556
|
+
"""Is a count about what a claim is about? Returns (covers, apart). A count names what it counts in the plural
|
|
557
|
+
("cargo routes", "fitting selections"); where the statement uses that word too, the words right before it on each
|
|
558
|
+
side say which kind: "cargo routes" are cargo routes. The count covers the claim if every kind it names is one the
|
|
559
|
+
statement names too: none of 3 cargo routes longer than 26 km says nothing of passenger routes, nor of every route
|
|
560
|
+
(audit HG-024-02). They are apart if each names a kind the other doesn't (cargo, passenger); a count of all routes
|
|
561
|
+
with one longer may still be about the passenger route. A count that names nothing the statement does is read as
|
|
562
|
+
before."""
|
|
563
|
+
said = re.findall(r"[a-z]+", stmt.lower())
|
|
564
|
+
stems = [L.content_words(w) for w in said]
|
|
565
|
+
about = set().union(*stems) if stems else set()
|
|
566
|
+
text = f"{m.get('name') or ''} . {str(m.get('key') or '').replace('_', ' ')}".lower()
|
|
567
|
+
words = re.findall(r"[a-z]+|\.", text)
|
|
568
|
+
mine = L.content_words(text)
|
|
569
|
+
covers, apart = True, False
|
|
570
|
+
for i, w in enumerate(words):
|
|
571
|
+
head = L.content_words(w)
|
|
572
|
+
plural = w.endswith("s") and not w.endswith("ss")
|
|
573
|
+
if not head or not plural or w in _DIRECTION_OF or w in _COUNTING | _HOW_MANY or not head & about:
|
|
574
|
+
continue
|
|
575
|
+
kinds = _modifiers(words, i) - about
|
|
576
|
+
theirs = set().union(*(_modifiers(said, j) for j, s in enumerate(stems) if s & head)) - mine
|
|
577
|
+
covers = covers and not kinds
|
|
578
|
+
apart = apart or bool(kinds and theirs)
|
|
579
|
+
return covers, apart
|
|
580
|
+
|
|
581
|
+
|
|
490
582
|
def _pinned(m: dict, rv: dict, figs: set[float]) -> bool:
|
|
491
583
|
"""A count at the claim's own figure: when it refutes the claim, it does whatever else is bound beside it."""
|
|
492
584
|
return bool(figs) and bool(_count_figures(m, rv, figs) & figs)
|
|
@@ -502,11 +594,19 @@ def _absolutes(stmt: str, scope: str, body: dict, bound: list) -> list[Problem]:
|
|
|
502
594
|
out: list[Problem] = []
|
|
503
595
|
seen: set[tuple] = set()
|
|
504
596
|
for w in words: # one check per thing the absolutes rule out
|
|
505
|
-
for
|
|
506
|
-
if (
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
597
|
+
for group in _target_groups(stmt, w):
|
|
598
|
+
if (tuple(group), _ALL.match(w) is None) in seen:
|
|
599
|
+
continue
|
|
600
|
+
seen.add((tuple(group), _ALL.match(w) is None))
|
|
601
|
+
got = [_absolute(stmt, scope, body, bound, w, words, set(figs), set(comps)) for figs, comps in group]
|
|
602
|
+
if len(group) > 1: # ruled out together: none of one part means none of both
|
|
603
|
+
if any(not g for g in got):
|
|
604
|
+
continue
|
|
605
|
+
got = [g for g in got if not any(p.code == "COUNT_CONTRADICTS" for p in g)][:1] or [[Problem(
|
|
606
|
+
"ABSOLUTE_WITHOUT_COUNT", f"'{w}' rules out what meets both comparisons at once: a count of each "
|
|
607
|
+
"on its own can't show that something meets both. Back it with a count of either one where k is "
|
|
608
|
+
"0, or count both together", fix="count what meets both comparisons together", words=words)]]
|
|
609
|
+
out += [p for g in got for p in g if p.text() not in {q.text() for q in out}]
|
|
510
610
|
if not any(p.code == "COUNT_CONTRADICTS" for p in out):
|
|
511
611
|
out += _superlative_contradicted(stmt, bound)
|
|
512
612
|
return out
|
|
@@ -514,7 +614,7 @@ def _absolutes(stmt: str, scope: str, body: dict, bound: list) -> list[Problem]:
|
|
|
514
614
|
|
|
515
615
|
def _absolute(stmt: str, scope: str, body: dict, bound: list, w: str, words: list, figs: set[float],
|
|
516
616
|
comps: set = frozenset()) -> list[Problem]:
|
|
517
|
-
arms = [(m, rv, a, role) for m, rv in bound if (a := _arm(rv)) and (role := _role(m, rv, a, figs, comps))]
|
|
617
|
+
arms = [(m, rv, a, role) for m, rv in bound if (a := _arm(rv)) and (role := _role(m, rv, a, figs, comps, stmt))]
|
|
518
618
|
about = L.content_words(stmt)
|
|
519
619
|
# a count at the claim's own figure that finds exceptions refutes it, whatever else is bound beside it
|
|
520
620
|
pinned = [(m, a) for m, rv, a, role in arms if role == "refute" and _pinned(m, rv, figs)
|
|
@@ -673,7 +773,7 @@ def _superlative_contradicted(stmt: str, bound: list) -> list[Problem]:
|
|
|
673
773
|
about = L.content_words(stmt)
|
|
674
774
|
for w, figs, comps in _extreme_targets(stmt, named=True):
|
|
675
775
|
rel = [(m, rv, a, role) for m, rv in bound if (a := _arm(rv))
|
|
676
|
-
and (role := _role(m, rv, a, set(figs), set(comps)))
|
|
776
|
+
and (role := _role(m, rv, a, set(figs), set(comps), stmt))
|
|
677
777
|
and (L.content_words(f"{m.get('name', '')} {m.get('key', '')}") & about
|
|
678
778
|
or _BEYOND.search(f"{m.get('name', '')} {m.get('key', '')}".replace("_", " "))
|
|
679
779
|
or stated_figure(rv) in figs)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Which counts bear on a claim, and what two joined comparisons rule out (audit of 0.7.24, HG-024-01..04).
|
|
2
|
+
|
|
3
|
+
A count of routes processed says nothing about routes longer than 26 km. A count of cargo routes says nothing about
|
|
4
|
+
passenger routes, nor about every route. "No route is shorter than 20 km and longer than 30 km" rules out a route that
|
|
5
|
+
is both, so a route shorter than 20 doesn't show it false; "every route is longer than 10 km and shorter than 30 km" is
|
|
6
|
+
false if either fails. And a count made with ">=" is not one of routes "longer than" its figure."""
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from hypgraph import Graph, GraphError, Policy
|
|
10
|
+
|
|
11
|
+
SCOPE = "The three surveyed routes, of 18, 25 and 35 km, against 10, 20, 26 and 30 km marks."
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def record(values):
|
|
15
|
+
g = Graph(policy=Policy(reviewers={"critic"}))
|
|
16
|
+
inv = g.author("curator").investigation("Routes", "Which surveyed route is shortest?")["id"]
|
|
17
|
+
lab = g.author("lab")
|
|
18
|
+
run = lab.add("run", inv, kind="lab", state="succeeded", name="Measured the surveyed routes.")
|
|
19
|
+
r = lab.add("result", inv, kind="computed", outcome="positive", summary="Route counts.",
|
|
20
|
+
values=values, links=[{"rel": "produced", "from": run["id"]}])["id"]
|
|
21
|
+
return g, inv, r
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def write(g, inv, statement, links, meas=(), scope=SCOPE):
|
|
25
|
+
return g.author("a").add("claim", inv, statement=statement, scope=scope, measurements=list(meas),
|
|
26
|
+
links=[{"rel": "supports", "from": x, "why": "Cited for the claim."} for x in links])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def codes(e) -> list[str]:
|
|
30
|
+
return [p["code"] for p in e.value.problems]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def count(r, key, name, k, n=3):
|
|
34
|
+
return {"name": name, "result": r, "key": key, "k": k, "n": n}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def typed(k, op=">", figure=26):
|
|
38
|
+
return {"k": k, "n": 3, "figure": figure, "op": op}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.mark.parametrize("statement,k", [("Nothing is longer than 26 km among the 3 routes.", 3),
|
|
42
|
+
("No route is longer than 26 km among the 3 routes.", 3),
|
|
43
|
+
("Every route is longer than 26 km among the 3 routes.", 0),
|
|
44
|
+
("Every route is longer than 26 km among the 3 routes.", 3)])
|
|
45
|
+
def test_a_count_with_no_comparison_does_not_back_one(statement, k):
|
|
46
|
+
"""HG-024-01: with no comparison of its own, a count of none or all backed any claim that ruled one out."""
|
|
47
|
+
g, inv, r = record({"processed": {"k": k, "n": 3}})
|
|
48
|
+
with pytest.raises(GraphError) as e:
|
|
49
|
+
write(g, inv, statement, [r], [count(r, "processed", "routes processed", k)])
|
|
50
|
+
assert codes(e) == ["ABSOLUTE_WITHOUT_COUNT"]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_a_claim_with_no_comparison_reads_a_plain_count_as_before():
|
|
54
|
+
g, inv, r = record({"failed": {"k": 0, "n": 3}})
|
|
55
|
+
write(g, inv, "No route ever failed to load among the 3 routes.", [r],
|
|
56
|
+
[count(r, "failed", "routes that failed to load", 0)])
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.mark.parametrize("statement", ["No passenger route is longer than 26 km among the 3 routes.",
|
|
60
|
+
"Nothing is longer than 26 km among the 3 passenger routes.",
|
|
61
|
+
"No route is longer than 26 km among the 3 routes."])
|
|
62
|
+
def test_a_count_of_another_kind_does_not_back_the_claim(statement):
|
|
63
|
+
"""HG-024-02: none of 3 cargo routes longer backed a claim about passenger routes (1 of 3 of them longer), and about
|
|
64
|
+
every route, because the count and the claim shared the word "route"."""
|
|
65
|
+
g, inv, r = record({"cargo_longer_26": typed(0), "passenger_longer_26": typed(1)})
|
|
66
|
+
with pytest.raises(GraphError) as e:
|
|
67
|
+
write(g, inv, statement, [r], [count(r, "cargo_longer_26", "cargo routes longer than 26 km", 0)])
|
|
68
|
+
assert codes(e) == ["ABSOLUTE_WITHOUT_COUNT"]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.mark.parametrize("name", ["passenger routes longer than 26 km", "routes longer than 26 km"])
|
|
72
|
+
def test_a_count_of_the_same_kind_or_all_of_them_backs_it(name):
|
|
73
|
+
g, inv, r = record({"longer_26": typed(0)})
|
|
74
|
+
write(g, inv, "No passenger route is longer than 26 km among the 3 routes.", [r],
|
|
75
|
+
[count(r, "longer_26", name, 0)])
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_a_count_of_one_kind_still_refutes_a_claim_about_all():
|
|
79
|
+
g, inv, r = record({"cargo_longer_26": typed(1)})
|
|
80
|
+
with pytest.raises(GraphError) as e:
|
|
81
|
+
write(g, inv, "No route is longer than 26 km among the 3 routes.", [r],
|
|
82
|
+
[count(r, "cargo_longer_26", "cargo routes longer than 26 km", 1)])
|
|
83
|
+
assert codes(e) == ["COUNT_CONTRADICTS"]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_two_kinds_named_apart_do_not_contradict():
|
|
87
|
+
g, inv, r = record({"cargo_longer_26": typed(1)})
|
|
88
|
+
with pytest.raises(GraphError) as e:
|
|
89
|
+
write(g, inv, "No passenger route is longer than 26 km among the 3 routes.", [r],
|
|
90
|
+
[count(r, "cargo_longer_26", "cargo routes longer than 26 km", 1)])
|
|
91
|
+
assert codes(e) == ["ABSOLUTE_WITHOUT_COUNT"]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
BOTH = {"shorter_20": typed(1, "<", 20), "longer_30": typed(1, ">", 30), "shorter_10": typed(0, "<", 10),
|
|
95
|
+
"longer_10": typed(3, ">", 10), "shorter_30": typed(2, "<", 30)}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_no_route_both_shorter_and_longer_is_not_refuted_by_either():
|
|
99
|
+
"""HG-024-03: "and" ended the clause, so a route shorter than 20 refuted a claim that rules out only a route both
|
|
100
|
+
shorter than 20 and longer than 30. With no count of both, it's unresolved."""
|
|
101
|
+
g, inv, r = record(BOTH)
|
|
102
|
+
with pytest.raises(GraphError) as e:
|
|
103
|
+
write(g, inv, "No route is shorter than 20 km and longer than 30 km among the 3 routes.", [r],
|
|
104
|
+
[count(r, "shorter_20", "routes shorter than 20 km", 1),
|
|
105
|
+
count(r, "longer_30", "routes longer than 30 km", 1)])
|
|
106
|
+
assert codes(e) == ["ABSOLUTE_WITHOUT_COUNT"]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_none_of_one_part_backs_none_of_both():
|
|
110
|
+
g, inv, r = record(BOTH)
|
|
111
|
+
write(g, inv, "No route is shorter than 10 km and longer than 30 km among the 3 routes.", [r],
|
|
112
|
+
[count(r, "shorter_10", "routes shorter than 10 km", 0)])
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_no_route_shorter_or_longer_is_refuted_by_either():
|
|
116
|
+
g, inv, r = record(BOTH)
|
|
117
|
+
with pytest.raises(GraphError) as e:
|
|
118
|
+
write(g, inv, "No route is shorter than 20 km or longer than 30 km among the 3 routes.", [r],
|
|
119
|
+
[count(r, "shorter_20", "routes shorter than 20 km", 1),
|
|
120
|
+
count(r, "longer_30", "routes longer than 30 km", 1)])
|
|
121
|
+
assert "COUNT_CONTRADICTS" in codes(e)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_every_route_between_two_marks_needs_both():
|
|
125
|
+
"""The same split left "shorter than 30 km" unread after "every route is longer than 10 km and": the claim went in
|
|
126
|
+
with 2 of 3 shorter than 30 bound beside it."""
|
|
127
|
+
g, inv, r = record(BOTH)
|
|
128
|
+
statement = "Every route is longer than 10 km and shorter than 30 km among the 3 routes."
|
|
129
|
+
with pytest.raises(GraphError) as e:
|
|
130
|
+
write(g, inv, statement, [r], [count(r, "longer_10", "routes longer than 10 km", 3)])
|
|
131
|
+
assert codes(e) == ["ABSOLUTE_WITHOUT_COUNT"]
|
|
132
|
+
with pytest.raises(GraphError) as e:
|
|
133
|
+
write(g, inv, statement, [r], [count(r, "longer_10", "routes longer than 10 km", 3),
|
|
134
|
+
count(r, "shorter_30", "routes shorter than 30 km", 2)])
|
|
135
|
+
assert codes(e) == ["COUNT_CONTRADICTS"]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_and_with_a_subject_of_its_own_still_ends_the_clause():
|
|
139
|
+
g, inv, r = record(BOTH)
|
|
140
|
+
with pytest.raises(GraphError) as e:
|
|
141
|
+
write(g, inv, "No route is shorter than 20 km and the register lists 3 routes.", [r],
|
|
142
|
+
[count(r, "shorter_20", "routes shorter than 20 km", 1)])
|
|
143
|
+
assert codes(e) == ["COUNT_CONTRADICTS"]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@pytest.mark.parametrize("op,name", [(">=", "routes longer than 26 km"), (">", "routes 26 km or longer"),
|
|
147
|
+
(">", "routes at least 26 km"), ("<", "routes no longer than 26 km")])
|
|
148
|
+
def test_a_name_must_say_the_boundary_the_count_used(op, name):
|
|
149
|
+
"""HG-024-04: only a name pointing the other way was caught; "longer than 26" on a ">=" count went in."""
|
|
150
|
+
g, inv, r = record({"routes_26": typed(2, op)})
|
|
151
|
+
with pytest.raises(GraphError) as e:
|
|
152
|
+
write(g, inv, "2 of 3 routes are at least 26 km long.", [r], [count(r, "routes_26", name, 2)])
|
|
153
|
+
assert "MEASUREMENT_MISMATCH" in codes(e)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@pytest.mark.parametrize("op,name", [(">=", "routes at least 26 km"), (">=", "routes 26 km or longer"),
|
|
157
|
+
(">", "routes longer than 26 km"), ("<=", "routes no longer than 26 km")])
|
|
158
|
+
def test_a_name_that_says_the_boundary_is_accepted(op, name):
|
|
159
|
+
g, inv, r = record({"routes_26": typed(2, op)})
|
|
160
|
+
write(g, inv, "2 of 3 routes are at least 26 km long.", [r], [count(r, "routes_26", name, 2)])
|
|
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
|