hypgraph 0.8.0__tar.gz → 0.8.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hypgraph-0.8.0 → hypgraph-0.8.1}/CHANGELOG.md +21 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/PKG-INFO +1 -1
- {hypgraph-0.8.0 → hypgraph-0.8.1}/docs/PLAN.md +1 -1
- {hypgraph-0.8.0 → hypgraph-0.8.1}/docs/RELEASE-READINESS.md +15 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/docs/spec.md +12 -5
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/__init__.py +1 -1
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/assertions.py +77 -21
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/checks.py +24 -3
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_assertions.py +127 -8
- {hypgraph-0.8.0 → hypgraph-0.8.1}/.gitignore +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/AGENTS.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/CITATION.cff +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/DISCLAIMER.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/LICENSE +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/NOTICE +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/README.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/docs/decisions/ADR-002-governance-profiles.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/docs/profile-workflows-plan.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/docs/review-2026-10-24.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/docs/site-plan.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/docs/ui-plan.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/docs/verification.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/docs/workflows/README.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/docs/workflows/knowledge.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/docs/workflows/optimization.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/docs/workflows/scientific.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/docs/workflows/software.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/docs/workflows/trading.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/examples/frameworks/README.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/pyproject.toml +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/__main__.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/blind.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/bundle.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/cli.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/coord.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/demos/__init__.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/demos/broad_street.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/demos/linked.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/derive.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/graph.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/language.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/mcp.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/plan.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/policy.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/profiles.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/references.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/refusals.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/report_check.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/schema.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/server.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/skill/SKILL.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/stats.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/store.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/ui/__init__.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/ui/atlas.css +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/ui/atlas.js +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/ui/hypgraph.css +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/ui/hypgraph.i18n.js +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/ui/hypgraph.js +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/ui/hypgraph.svg +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/ui/index.html +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/ui/vendor/cose-base.js +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/ui/vendor/layout-base.js +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/src/hypgraph/workflow.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_accept_work_kind.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_anchoring.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_blind.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_blind_oracles.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_borrowed_words.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_bound_support.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_bundle_telemetry.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_cancel_releases_lease.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_cited_and_failed_support.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_contradictions.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_coord.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_counts_that_bear.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_d10.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_direction_and_units.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_engine.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_engine_fixes.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_figure_said_twice.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_framework_examples.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_i18n.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_incremental.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_link_translate.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_linked_records.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_log_and_check.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_measure_questions.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_opposed_answers.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_option_lead.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_order_detail.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_profiles.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_profiles_041.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_quantifiers_and_identity.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_questions_not_assertions.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_references.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_refusal_friction.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_refusal_results.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_refusals.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_relation_words.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_report_answers.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_rerunnable.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_restates_scale.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_server_auth.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_server_hosts.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_shape_refusals.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_stats_not_backtests.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_stats_values.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_surfaces.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_telemetry_stream.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_trust_boundary.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_typed_audits.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_ui_app.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_ui_engine.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_uncorrected_effect.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_unnamed_inputs.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_verify_complete.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_verify_lifecycle.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_verify_scope.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_verify_scoped_views.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_verify_trust.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_verify_views.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_versions_not_figures.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_vocabulary_friction.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_what_a_count_is_of.py +0 -0
- {hypgraph-0.8.0 → hypgraph-0.8.1}/tests/test_workflows.py +0 -0
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.1 — 2026-11-01: exact figures, units within their kind
|
|
4
|
+
|
|
5
|
+
An independent audit of 0.8.0 found two defects in the typed checks, both confirmed on the published wheel.
|
|
6
|
+
|
|
7
|
+
- **Exact figures (HG-080-01).** Containment of two conditions was decided by probing at each figure ± 1 and the
|
|
8
|
+
midpoints, in floats. Above 2**53 a figure + 1 is the figure, so x > 1e18 looked inside x <= 1e18, and a count of 0
|
|
9
|
+
above 1e18 supported "none below 1e18". Containment is now decided on the pieces the two figures cut the line into,
|
|
10
|
+
with no arithmetic on them, and figures keep their exact value (10**18 + 1 is not 1e18; it renders as itself).
|
|
11
|
+
- **Units within their kind (HG-080-02).** The unit table's kind was dropped, so `lb` (mass) matched `GBP` and
|
|
12
|
+
`sterling` (currency) through the shared name "pound". A unit is now its (kind, unit) readings: `lb` = `lbs` and
|
|
13
|
+
`GBP` = `sterling` still, never across kinds, and an ambiguous `pound` matches only another `pound`; the refusal says
|
|
14
|
+
what it could be and asks for `lb` or `GBP`.
|
|
15
|
+
- **`exactly` decided from bounds.** It was left unresolved unless its set was the counted one or its complement:
|
|
16
|
+
4 of 4 above F now contradicts "exactly 4 below F". Over one population every verdict now equals enumeration of the
|
|
17
|
+
consistent worlds (sound and complete), and the test checks that.
|
|
18
|
+
- **An extreme stated as a value is two atoms.** In a typed investigation "the longest of the 4 routes is 26 km"
|
|
19
|
+
needs both "none above 26" and "some at 26 or more"; one of them is refused (`ASSERTION_MISSING`). An extreme stated
|
|
20
|
+
as a bound ("the slowest took at least 40 minutes") needs one.
|
|
21
|
+
- Tests: containment against exact rational probes over large, adjacent, subnormal and signed-zero figures; the audit's
|
|
22
|
+
cases through `_decide` and the write gate; units; extremes. The audit's own test file passes (13 of 13).
|
|
23
|
+
|
|
3
24
|
## 0.8.0 — 2026-11-01: typed claim assertions
|
|
4
25
|
|
|
5
26
|
Eight independent audits (HG-020 → HG-028) found the same class of failure, each time in a new wording: to decide
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hypgraph
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.1
|
|
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.8.
|
|
112
|
+
## Status at 0.8.1 (2026-11-01)
|
|
113
113
|
|
|
114
114
|
All seven phases are done: the library, bundles, the server and UI, the MCP server, the website, Pegasus running on
|
|
115
115
|
the package, and releases to PyPI from tags with trusted publishing. What came after the plan, by release, is in
|
|
@@ -234,6 +234,21 @@ was taken from a changelog without being checked against the code.*
|
|
|
234
234
|
> verified, and the record shows the atoms, not the sentence, as what it checked. Replaying the 1,900 recorded
|
|
235
235
|
> adversarial entries changes none. #102 is merged with green CI (17 of 17 checks). Released as 0.8.0.
|
|
236
236
|
|
|
237
|
+
> **Audit of 0.8.0 (2026-11-01).** The first audit of the typed layer found its architecture sound (GO for a
|
|
238
|
+
> controlled typed-claims pilot) and two narrow defects in its helpers, both reproduced on the published wheel (its
|
|
239
|
+
> test file: 9 failed, the 4 controls passed; the wheel's SHA-256 matched the audit's). Containment was probed in
|
|
240
|
+
> floats at figure ± 1, which above 2**53 is the figure itself, so opposite conditions at 1e18 looked equal and an
|
|
241
|
+
> unsupported "none below 1e18" was supported (HG-080-01). And units dropped their kind, so `lb` matched `GBP` through
|
|
242
|
+
> the shared name "pound" (HG-080-02). Containment is now decided exactly on the pieces of the line, with figures kept
|
|
243
|
+
> as given; a unit is its (kind, unit) readings. Two related gaps closed with them: `exactly` was left open unless its
|
|
244
|
+
> set was the counted one (now decided from bounds, and every verdict equals enumeration over one population), and an
|
|
245
|
+
> extreme stated as a value now needs both its atoms.
|
|
246
|
+
>
|
|
247
|
+
> **Where that stands.** The boundary the audit names stays: prose is matched to atoms by reading words, so a missed
|
|
248
|
+
> part leaves prose unverified, never an atom verified; integrations should consume `asserts` and their verdicts, not
|
|
249
|
+
> the statement. Untyped mode keeps its documented limits. #104 is merged with green CI (17 of 17 checks). Released as
|
|
250
|
+
> 0.8.1.
|
|
251
|
+
|
|
237
252
|
## Recommendation: CONDITIONAL GO
|
|
238
253
|
|
|
239
254
|
HypGraph is ready for a public developer preview once #59, #60 and #61 are merged with green CI and released as one
|
|
@@ -290,22 +290,29 @@ smallest detectable difference uses 80% power at two-sided α = 0.05.
|
|
|
290
290
|
|
|
291
291
|
**Typed claims.** An atom (§2) is decided by its bound count alone; no words are read. Let S_e = {x : x op_e f_e} (what
|
|
292
292
|
was counted, k of n inside it, n − k outside) and S_a = {x : x op_a f_a} (what is asserted); P_e and P_a the
|
|
293
|
-
populations. The property must equal the count's and the unit match it
|
|
294
|
-
|
|
293
|
+
populations. The property must equal the count's and the unit match it: spellings of one unit are one unit within one
|
|
294
|
+
kind of unit (`lb` = `lbs`, `GBP` = `sterling`, never `lb` = `GBP`), a spelling that could be two kinds (`pound`:
|
|
295
|
+
mass or currency) matches only another that could be the same two, and nothing is converted. A stated `n` must equal
|
|
296
|
+
the count's. Figures are compared exactly, as given (an int or a float, at any magnitude), on the pieces of the line
|
|
297
|
+
the two figures cut it into — never by arithmetic on them.
|
|
295
298
|
|
|
296
299
|
- `none` S_a: **supported** when a side of the count with 0 inside it contains S_a and P_a lies within P_e;
|
|
297
300
|
**contradicted** when a side with more than 0 lies inside S_a and P_e lies within P_a.
|
|
298
301
|
- `some` S_a: supported when a side with more than 0 lies inside S_a and P_e within P_a; contradicted when a side with
|
|
299
302
|
0 contains S_a and P_a lies within P_e.
|
|
300
|
-
- `all` S_a is `none` of the complement of S_a.
|
|
301
|
-
|
|
303
|
+
- `all` S_a is `none` of the complement of S_a.
|
|
304
|
+
- `exactly` k needs the same population. A side of the count lying inside S_a puts at least its count in S_a; a side
|
|
305
|
+
containing S_a, at most its count. k at both bounds is **supported**, k outside them **contradicted**.
|
|
306
|
+
|
|
307
|
+
Over one population these verdicts are complete as well as sound: supported or contradicted exactly when every set of
|
|
308
|
+
values consistent with the count agrees.
|
|
302
309
|
- Anything else is **unresolved**, and names the field that blocked it.
|
|
303
310
|
|
|
304
311
|
| Code | Refused when |
|
|
305
312
|
|---|---|
|
|
306
313
|
| `ASSERTION_CONTRADICTED` | an atom is false by its count. The refusal log tracks it like a count contradiction (§8). |
|
|
307
314
|
| `ASSERTION_UNRESOLVED` | an atom its count can't settle: another property or unit, a population neither within nor containing the count's, a condition the count's doesn't decide, an untyped count, a malformed atom. |
|
|
308
|
-
| `ASSERTION_MISSING` | in a `typed_claims` investigation, the statement
|
|
315
|
+
| `ASSERTION_MISSING` | in a `typed_claims` investigation, the statement's parts that assert an absolute, an extreme or a count need more atoms than the claim has: one each, and two for an extreme stated as a value ("the longest is 26 km": none above, some at or above). Detection reads words only to ask for atoms; a missed word leaves prose unverified, never an atom verified. The refusal proposes atoms drafted from the statement and the claim's typed counts, which are checked like any other once written. |
|
|
309
316
|
|
|
310
317
|
Atoms are checked in every investigation that has them. In a `typed_claims` investigation the word interpretation
|
|
311
318
|
stands down: a measurement's name and key are not read for what was counted, and rule 5 (absolutes, with the
|
|
@@ -16,7 +16,7 @@ Standing is derived on every read, never stored; every write is an event in a SH
|
|
|
16
16
|
their numbers; reviews pin versions; independence is counted, not assumed. See https://hypgraph.com.
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
-
__version__ = "0.8.
|
|
19
|
+
__version__ = "0.8.1"
|
|
20
20
|
|
|
21
21
|
from .bundle import anchored, verify_bundle # noqa: E402
|
|
22
22
|
from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
|
|
@@ -18,7 +18,9 @@ steered one of those inferences. Here the instrument types what it counted and t
|
|
|
18
18
|
population a path: ``route/passenger`` lies within ``route``, so a count of 0 over every route also settles
|
|
19
19
|
"no passenger route"; a count of the passenger routes says nothing of the cargo routes.
|
|
20
20
|
property an id, compared for equality ("length" is not "speed").
|
|
21
|
-
unit compared for equality, spellings of one unit read as one ("km" = "kilometres")
|
|
21
|
+
unit compared for equality, spellings of one unit read as one ("km" = "kilometres") within one kind of unit:
|
|
22
|
+
"lb" is mass and "GBP" currency, never the same; "pound", which could be either, matches only
|
|
23
|
+
another spelling that could be either. Never converted.
|
|
22
24
|
n optional: the population's size as the claim states it; must be the count's n.
|
|
23
25
|
|
|
24
26
|
Each atom is decided on its own, by sets on the number line — S_e = {x : x op_e figure_e} (what was counted) and
|
|
@@ -28,7 +30,13 @@ S_a = {x : x op_a figure_a} (what is asserted) — and the paths. A count of k i
|
|
|
28
30
|
contradicted when a count above 0 is over a set inside S_a and a population inside the atom's
|
|
29
31
|
some S_a the mirror: a count above 0 inside S_a supports it, a count of 0 containing S_a contradicts it
|
|
30
32
|
all S_a none of the complement of S_a
|
|
31
|
-
exactly the same set
|
|
33
|
+
exactly the same population; a count over a set inside S_a puts at least that many in it, a count over a
|
|
34
|
+
set containing S_a at most that many: k inside those bounds, at both ends supported, outside them
|
|
35
|
+
contradicted (4 of 4 above F puts at most 0 below it, so "exactly 4 below F" is false)
|
|
36
|
+
|
|
37
|
+
Figures are compared exactly, at any magnitude: containment is decided on the pieces of the number line the two
|
|
38
|
+
figures cut it into (the figures themselves and the open intervals between), with no arithmetic on the figures — a
|
|
39
|
+
probe at figure + 1 is the figure itself above 2**53, which made x > 1e18 look inside x <= 1e18 (HG-080-01).
|
|
32
40
|
|
|
33
41
|
Anything else is unresolved, and the verdict names the field that blocked it. One count can back two atoms only if
|
|
34
42
|
each checks out against it on its own, so a true absolute can't lend anything to a false one (HG-028-01), and "all"
|
|
@@ -76,29 +84,56 @@ def prop(p) -> str | None:
|
|
|
76
84
|
|
|
77
85
|
|
|
78
86
|
def _unit(u) -> frozenset | None:
|
|
79
|
-
"""A unit as the
|
|
87
|
+
"""A unit as the (kind, unit) pairs it could be ("km" → {(length, kilometre)}, "pound" → {(mass, pound),
|
|
88
|
+
(currency, pound)}); an unknown spelling is itself. None when there's none."""
|
|
80
89
|
if u is None or (isinstance(u, str) and not u.strip()):
|
|
81
90
|
return None
|
|
82
91
|
from .checks import _UNIT_OF # one table of unit spellings, shared with the word checks
|
|
83
92
|
w = str(u).strip().lower()
|
|
84
|
-
return frozenset(
|
|
93
|
+
return frozenset(_UNIT_OF.get(w, ())) or frozenset({("", w)})
|
|
85
94
|
|
|
86
95
|
|
|
87
96
|
def same_unit(a, b) -> bool:
|
|
88
|
-
|
|
89
|
-
|
|
97
|
+
"""One unit: every reading of one is a reading of the other. "lb" = "lbs" and "GBP" = "sterling", but "lb" is
|
|
98
|
+
never "GBP" (HG-080-02), and an ambiguous "pound" is neither of them."""
|
|
99
|
+
return _unit(a) == _unit(b)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def ambiguous(u) -> list[str]:
|
|
103
|
+
"""The kinds of unit an ambiguous spelling could be ("pound": currency, mass); [] when it is one unit."""
|
|
104
|
+
kinds = sorted({k for k, _c in _unit(u) or ()})
|
|
105
|
+
return kinds if len(kinds) > 1 else []
|
|
90
106
|
|
|
91
107
|
|
|
92
108
|
def holds(x: float, op: str, f: float) -> bool:
|
|
93
109
|
return {"<": x < f, "<=": x <= f, "=": x == f, "!=": x != f, ">=": x >= f, ">": x > f}[op]
|
|
94
110
|
|
|
95
111
|
|
|
112
|
+
def _pieces(*figures) -> list[tuple]:
|
|
113
|
+
"""The number line cut at the figures: each figure as a point ("pt", x) and the open intervals between and beyond
|
|
114
|
+
them ("iv", lo, hi), None for an infinite end. Figures are compared as given (Python compares ints and floats
|
|
115
|
+
exactly); nothing is added to or averaged from them."""
|
|
116
|
+
pts = sorted(set(figures))
|
|
117
|
+
out: list[tuple] = [("iv", None, pts[0])]
|
|
118
|
+
for x, y in zip(pts, pts[1:]):
|
|
119
|
+
out += [("pt", x), ("iv", x, y)]
|
|
120
|
+
return out + [("pt", pts[-1]), ("iv", pts[-1], None)]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _inside(piece: tuple, op: str, f) -> bool:
|
|
124
|
+
"""Is a piece of the line inside {x : x op f}? f is one of the cuts, so an open interval lies wholly below or
|
|
125
|
+
wholly above it, and which side decides."""
|
|
126
|
+
if piece[0] == "pt":
|
|
127
|
+
return holds(piece[1], op, f)
|
|
128
|
+
_iv, _lo, hi = piece
|
|
129
|
+
below = hi is not None and hi <= f
|
|
130
|
+
return op in ("<", "<=", "!=") if below else op in (">", ">=", "!=")
|
|
131
|
+
|
|
132
|
+
|
|
96
133
|
def subset(a: tuple, b: tuple) -> bool:
|
|
97
|
-
"""Is {x : x a[0] a[1]} inside {x : x b[0] b[1]}? Both sets are unions of
|
|
98
|
-
so
|
|
99
|
-
|
|
100
|
-
probes = pts + [pts[0] - 1, pts[-1] + 1] + [(x + y) / 2 for x, y in zip(pts, pts[1:])]
|
|
101
|
-
return all(holds(x, *b) for x in probes if holds(x, *a))
|
|
134
|
+
"""Is {x : x a[0] a[1]} inside {x : x b[0] b[1]}? Both sets are unions of the pieces the two figures cut the line
|
|
135
|
+
into, so it is decided piece by piece, exactly, at any magnitude."""
|
|
136
|
+
return all(_inside(p, *b) for p in _pieces(a[1], b[1]) if _inside(p, *a))
|
|
102
137
|
|
|
103
138
|
|
|
104
139
|
def same_set(a: tuple, b: tuple) -> bool:
|
|
@@ -122,7 +157,7 @@ def count_fields(rv) -> tuple[dict | None, str]:
|
|
|
122
157
|
pop = path(rv["population"])
|
|
123
158
|
if pop is None:
|
|
124
159
|
return None, "the count's population is not a path (route/passenger)"
|
|
125
|
-
return {"k": int(k), "n": int(n), "op": rv["op"], "figure":
|
|
160
|
+
return {"k": int(k), "n": int(n), "op": rv["op"], "figure": rv["figure"], "population": pop,
|
|
126
161
|
"property": prop(rv["property"]), "unit": rv.get("unit")}, ""
|
|
127
162
|
|
|
128
163
|
|
|
@@ -171,9 +206,18 @@ def _cond(a: dict) -> str:
|
|
|
171
206
|
return f"{a['property']} {SHOW_OP.get(a['op'], a['op'])} {fig}{u}"
|
|
172
207
|
|
|
173
208
|
|
|
209
|
+
def whole_form(f):
|
|
210
|
+
"""A figure with its exact value, an integral float as an int (26.0 → 26, 1e18 → 10**18): 26 and 26.0 are one
|
|
211
|
+
figure. (Not ``f.is_integer()`` on an int — that is Python 3.12+.)"""
|
|
212
|
+
return f if isinstance(f, int) else int(f) if float(f).is_integer() else f
|
|
213
|
+
|
|
214
|
+
|
|
174
215
|
def stats_fmt(v) -> str:
|
|
216
|
+
"""A figure as it is: ints whole, floats exact (repr round-trips), so a rendered figure never hides a difference."""
|
|
217
|
+
if isinstance(v, int):
|
|
218
|
+
return str(v)
|
|
175
219
|
v = float(v)
|
|
176
|
-
return str(int(v)) if v.is_integer() else
|
|
220
|
+
return str(int(v)) if v.is_integer() and abs(v) < 1e15 else repr(v)
|
|
177
221
|
|
|
178
222
|
|
|
179
223
|
def render(a: dict) -> str:
|
|
@@ -213,8 +257,11 @@ def _decide(a: dict, e: dict) -> tuple[str, str, str]:
|
|
|
213
257
|
return "unresolved", "property", (f"the count is of {e['property']}, the assertion is about "
|
|
214
258
|
f"{prop(a['property'])}")
|
|
215
259
|
if not same_unit(a.get("unit"), e.get("unit")):
|
|
260
|
+
amb = next(((u, ambiguous(u)) for u in (a.get("unit"), e.get("unit")) if ambiguous(u)), None)
|
|
216
261
|
return "unresolved", "unit", (f"the count is in {e.get('unit') or 'no unit'}, the assertion in "
|
|
217
|
-
f"{a.get('unit') or 'no unit'}: state the figure in the count's unit"
|
|
262
|
+
f"{a.get('unit') or 'no unit'}: state the figure in the count's unit"
|
|
263
|
+
+ (f"; '{amb[0]}' could be {' or '.join(amb[1])}, so name the unit "
|
|
264
|
+
"unambiguously (lb, GBP)" if amb else ""))
|
|
218
265
|
if not (within(pa, pe) or within(pe, pa)):
|
|
219
266
|
return "unresolved", "population", (f"the count is of {_pop(pe)}, the assertion is about {_pop(pa)}: "
|
|
220
267
|
"neither lies within the other")
|
|
@@ -225,7 +272,7 @@ def _decide(a: dict, e: dict) -> tuple[str, str, str]:
|
|
|
225
272
|
f"{_pop(pe)} ({e['n']}) doesn't give")
|
|
226
273
|
# what was counted, both ways: k inside S_e and n - k outside it
|
|
227
274
|
sides = [((e["op"], e["figure"]), e["k"]), ((COMPLEMENT[e["op"]], e["figure"]), e["n"] - e["k"])]
|
|
228
|
-
q, t = a["quantifier"], (a["op"],
|
|
275
|
+
q, t = a["quantifier"], (a["op"], a["figure"])
|
|
229
276
|
got = _settle(q, t, sides, pa, pe, a)
|
|
230
277
|
if got:
|
|
231
278
|
return got
|
|
@@ -242,11 +289,18 @@ def _settle(q: str, t: tuple, sides: list, pa: tuple, pe: tuple, a: dict) -> tup
|
|
|
242
289
|
if q == "exactly":
|
|
243
290
|
if pa != pe:
|
|
244
291
|
return None
|
|
292
|
+
lo, hi = 0, sum(c for _side, c in sides)
|
|
245
293
|
for side, c in sides:
|
|
246
|
-
if
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
294
|
+
if subset(side, t): # everything counted there is in the atom's set
|
|
295
|
+
lo = max(lo, c)
|
|
296
|
+
if subset(t, side): # the atom's set lies inside what was counted there
|
|
297
|
+
hi = min(hi, c)
|
|
298
|
+
want = int(a["k"])
|
|
299
|
+
found = f"the count finds {lo}" if lo == hi else f"the count puts {lo} to {hi} there"
|
|
300
|
+
if lo == hi == want:
|
|
301
|
+
return "supported", "", found
|
|
302
|
+
if want < lo or want > hi:
|
|
303
|
+
return "contradicted", "k", f"{found}, not {want}"
|
|
250
304
|
return None
|
|
251
305
|
neg = "that don't" if q == "all" else "that do"
|
|
252
306
|
if q == "all": # everything meets it: nothing meets its complement
|
|
@@ -308,8 +362,10 @@ def key(a) -> str:
|
|
|
308
362
|
if atom_shape(a, None)[0]:
|
|
309
363
|
return json.dumps(a, sort_keys=True, default=str)[:300]
|
|
310
364
|
u = _unit(a.get("unit"))
|
|
365
|
+
f = a["figure"]
|
|
366
|
+
fig = whole_form(f) # 26 and 26.0 are one figure, exactly
|
|
311
367
|
return json.dumps([a["quantifier"], "/".join(path(a["population"]) or ()), prop(a["property"]), a["op"],
|
|
312
|
-
|
|
368
|
+
fig, sorted(u) if u else None, a.get("n"), a.get("k")], default=str)
|
|
313
369
|
|
|
314
370
|
|
|
315
371
|
def numbers(body: dict) -> list[float]:
|
|
@@ -375,7 +431,7 @@ def draft(s: Snap, body: dict, inv: str = "") -> list[dict]:
|
|
|
375
431
|
pool = [(i, e) for i, e in typed if e["n"] == n]
|
|
376
432
|
for i, e in [x for x in pool if x[1]["figure"] in figs] or pool:
|
|
377
433
|
add({"quantifier": "exactly", "population": _pop(e["population"]), "property": e["property"],
|
|
378
|
-
"op": e["op"], "figure":
|
|
434
|
+
"op": e["op"], "figure": whole_form(e["figure"]),
|
|
379
435
|
"k": k, "n": n, "measurement": i}, e)
|
|
380
436
|
return out[:6]
|
|
381
437
|
|
|
@@ -1841,11 +1841,28 @@ def asserting_parts(stmt: str) -> list[str]:
|
|
|
1841
1841
|
if L.interrogative(p):
|
|
1842
1842
|
continue
|
|
1843
1843
|
quantified = any(_clause_comparisons(m.group("rest"), after_no=True) for m in _QUANT_CMP.finditer(p))
|
|
1844
|
-
if L.asserted(L.ABSOLUTE, p) or _extremes(p) or _STATED_COUNT.search(p) or quantified or
|
|
1844
|
+
if L.asserted(L.ABSOLUTE, p) or _extremes(p) or _STATED_COUNT.search(p) or quantified or _NO_NOUN_VERB.search(p):
|
|
1845
1845
|
out.append(p.strip())
|
|
1846
1846
|
return out
|
|
1847
1847
|
|
|
1848
1848
|
|
|
1849
|
+
_STATED_FIGURE = re.compile(r"(?<![\w.])\d[\d,]*(?:\.\d+)?")
|
|
1850
|
+
_POPULATION_SIZE = re.compile(r"\b(?:of|among|across|between|from|in|all)\s+(?:the\s+|all\s+|its\s+|these\s+|those\s+)?"
|
|
1851
|
+
r"(\d[\d,]*)\b(?!\s*(?:%|percent|per\s*cent))", re.I)
|
|
1852
|
+
|
|
1853
|
+
|
|
1854
|
+
def atoms_needed(part: str) -> int:
|
|
1855
|
+
"""How many atoms a part asks for: an extreme stated as a value ("the longest of the 4 routes is 26 km") is two —
|
|
1856
|
+
nothing above 26 and something at 26 or more; a figure it states only as a bound ("the slowest took at least 40
|
|
1857
|
+
minutes") is one. Over-counting asks for one more atom; it never marks one verified."""
|
|
1858
|
+
if not _extremes(part):
|
|
1859
|
+
return 1
|
|
1860
|
+
bounds = {f for f, _c in _clause_comparisons(part)}
|
|
1861
|
+
sizes = {m.start(1) for m in _POPULATION_SIZE.finditer(part)} # "of the 4 routes" is who, not a figure
|
|
1862
|
+
stated = {float(m.group(0).replace(",", "")) for m in _STATED_FIGURE.finditer(part) if m.start() not in sizes}
|
|
1863
|
+
return 2 if stated - bounds else 1
|
|
1864
|
+
|
|
1865
|
+
|
|
1849
1866
|
def assertion_issues(s: Snap, body: dict, inv: str, stmt: str, typed: bool) -> list[Problem]:
|
|
1850
1867
|
"""The claim's atoms checked against their counts, and — in a typed investigation — whether the statement asserts
|
|
1851
1868
|
more than its atoms do."""
|
|
@@ -1866,13 +1883,17 @@ def assertion_issues(s: Snap, body: dict, inv: str, stmt: str, typed: bool) -> l
|
|
|
1866
1883
|
f"{v['field']}", words=[v["field"]]))
|
|
1867
1884
|
if typed:
|
|
1868
1885
|
parts = asserting_parts(stmt)
|
|
1869
|
-
|
|
1886
|
+
needed = sum(atoms_needed(p) for p in parts)
|
|
1887
|
+
if needed > len(atoms):
|
|
1870
1888
|
proposed = A.draft(s, body, inv) if not atoms else []
|
|
1871
1889
|
hint = (" Proposed from the statement and its typed counts — check each before using it: "
|
|
1872
1890
|
+ json.dumps(proposed, separators=(",", ":"))[:900]) if proposed else ""
|
|
1873
1891
|
out.append(Problem("ASSERTION_MISSING", f"this investigation checks claims as typed assertions: the "
|
|
1874
1892
|
f"statement asserts {len(parts)} thing{'s' * (len(parts) != 1)} — "
|
|
1875
|
-
+ "; ".join(f"'{p[:80]}'" for p in parts[:4]) + f" —
|
|
1893
|
+
+ "; ".join(f"'{p[:80]}'" for p in parts[:4]) + f" — {needed} atom"
|
|
1894
|
+
+ "s" * (needed != 1) + " in all" + (" (an extreme stated as a value is two: none "
|
|
1895
|
+
"above it, some at it or beyond)" if needed > len(parts) else "")
|
|
1896
|
+
+ f", and the claim gives {len(atoms)} "
|
|
1876
1897
|
"atom" + "s" * (len(atoms) != 1) + ". Add an atom for each to asserts: {quantifier "
|
|
1877
1898
|
"(none | all | some | exactly), population, property, op, figure, unit, n, "
|
|
1878
1899
|
"measurement}." + hint, fix="add an atom to asserts for each thing the statement "
|
|
@@ -7,7 +7,9 @@ is decided by sets on the number line and population paths. Invariants: a verdic
|
|
|
7
7
|
never on the sentence around them; a true atom never lends anything to a false one; a count is whole."""
|
|
8
8
|
import itertools
|
|
9
9
|
import json
|
|
10
|
+
import math
|
|
10
11
|
import random
|
|
12
|
+
from fractions import Fraction
|
|
11
13
|
|
|
12
14
|
import pytest
|
|
13
15
|
|
|
@@ -75,16 +77,17 @@ def brute(q, op_a, f_a, op_e, f_e, k, n, kk=None):
|
|
|
75
77
|
@pytest.mark.parametrize("op_a", A.OPS)
|
|
76
78
|
@pytest.mark.parametrize("op_e", A.OPS)
|
|
77
79
|
@pytest.mark.parametrize("f_a", [20, 26, 30])
|
|
78
|
-
def
|
|
79
|
-
"""Sound:
|
|
80
|
-
|
|
80
|
+
def test_the_interval_logic_decides_exactly_what_enumeration_decides(q, op_a, op_e, f_a):
|
|
81
|
+
"""Sound and complete over one population: supported or contradicted exactly when every consistent world agrees,
|
|
82
|
+
unresolved exactly when they differ. (0.8.0 left 'exactly' open unless its set was the counted one or its
|
|
83
|
+
complement; it is now decided from bounds — HG-080.)"""
|
|
81
84
|
n = 3
|
|
82
85
|
for k in range(n + 1):
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
for kk in (range(n + 1) if q == "exactly" else [None]):
|
|
87
|
+
kw = {"k": kk} if q == "exactly" else {}
|
|
88
|
+
got = A._decide(atom(q, op_a, f_a, **kw), A.count_fields(cnt(k, 26, op_e, n))[0])[0]
|
|
89
|
+
want = brute(q, op_a, f_a, op_e, 26, k, n, kk)
|
|
90
|
+
assert got == want, (q, op_a, f_a, op_e, k, kk, got, want)
|
|
88
91
|
|
|
89
92
|
|
|
90
93
|
@pytest.mark.parametrize("q", ["none", "all", "some"])
|
|
@@ -388,3 +391,119 @@ def test_a_drafted_lie_is_still_refused():
|
|
|
388
391
|
"measurements": [{"result": r, "key": "over_26"}]}
|
|
389
392
|
d = A.draft(g.snapshot(), body, inv)
|
|
390
393
|
assert [v["verdict"] for v in A.verify(g.snapshot(), {**body, "asserts": d}, inv)] == ["contradicted"]
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# ── HG-080-01: exact at any magnitude ───────────────────────────────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
TRICKY = [10**16, 10**18, 10**24, 1e18, 10**18 + 1, -10**18, 2**53, 2**53 + 1, 5e-324, 0, -0.0, 1.0,
|
|
399
|
+
math.nextafter(1.0, 2.0), 26, 26.0]
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def exact_subset(a, b):
|
|
403
|
+
"""The reference: the probe method in exact rational arithmetic, where a figure ± 1 and a midpoint are always new
|
|
404
|
+
points (in floats, above 2**53 they are the figure itself)."""
|
|
405
|
+
fa, fb = Fraction(a[1]), Fraction(b[1])
|
|
406
|
+
pts = sorted({fa, fb})
|
|
407
|
+
probes = pts + [pts[0] - 1, pts[-1] + 1] + [(x + y) / 2 for x, y in zip(pts, pts[1:])]
|
|
408
|
+
return all(A.holds(x, b[0], fb) for x in probes if A.holds(x, a[0], fa))
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
@pytest.mark.parametrize("fa", TRICKY)
|
|
412
|
+
@pytest.mark.parametrize("fb", TRICKY)
|
|
413
|
+
def test_containment_is_exact_at_any_magnitude(fa, fb):
|
|
414
|
+
for oa in A.OPS:
|
|
415
|
+
for ob in A.OPS:
|
|
416
|
+
assert A.subset((oa, fa), (ob, fb)) == exact_subset((oa, fa), (ob, fb)), (oa, fa, ob, fb)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@pytest.mark.parametrize("f", [10**16, 10**18, 1e18, 10**24])
|
|
420
|
+
def test_a_large_figure_decides_as_a_small_one(f):
|
|
421
|
+
def e(k, op):
|
|
422
|
+
return A.count_fields(cnt(k, f, op))[0]
|
|
423
|
+
assert not A.subset((">", f), ("<=", f)) and not A.same_set((">", f), ("<", f))
|
|
424
|
+
assert A._decide(atom("none", "<", f, n=4), e(0, ">"))[0] == "unresolved" # 0 above F says nothing below
|
|
425
|
+
assert A._decide(atom("exactly", "<", f, n=4, k=4), e(4, ">"))[0] == "contradicted"
|
|
426
|
+
assert A._decide(atom("none", ">", f, n=4), e(0, ">"))[0] == "supported"
|
|
427
|
+
assert A._decide(atom("some", ">=", f), e(1, ">"))[0] == "supported"
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def test_one_past_a_large_figure_is_another_figure():
|
|
431
|
+
"""10**18 + 1 is not 1e18, though float() makes them one: nothing over 10**18 + 1 leaves room above 10**18."""
|
|
432
|
+
assert A._decide(atom("none", ">", 10**18, n=4), A.count_fields(cnt(0, 10**18 + 1))[0])[0] == "unresolved"
|
|
433
|
+
assert A._decide(atom("none", ">", 10**18 + 1, n=4), A.count_fields(cnt(0, 10**18))[0])[0] == "supported"
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def test_a_large_figure_through_the_write_gate():
|
|
437
|
+
values = {"over": cnt(0, 10**18, ">", unit=None)}
|
|
438
|
+
codes = write(values, "No route is shorter than 1000000000000000000 among the 4 routes.",
|
|
439
|
+
[atom("none", "<", 10**18, unit=None, n=4)], ["over"])
|
|
440
|
+
assert "ASSERTION_UNRESOLVED" in codes, codes
|
|
441
|
+
values = {"over": cnt(4, 10**18, ">", unit=None)}
|
|
442
|
+
codes = write(values, "Exactly 4 of the 4 routes are shorter than 1000000000000000000.",
|
|
443
|
+
[atom("exactly", "<", 10**18, unit=None, n=4, k=4)], ["over"])
|
|
444
|
+
assert "ASSERTION_CONTRADICTED" in codes, codes
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def test_a_figure_renders_as_it_is():
|
|
448
|
+
assert A.stats_fmt(10**18 + 1) == "1000000000000000001" and A.stats_fmt(26.0) == "26"
|
|
449
|
+
assert A.stats_fmt(math.nextafter(1.0, 2.0)) == "1.0000000000000002"
|
|
450
|
+
assert A.key(atom("none", figure=10**18)) != A.key(atom("none", figure=10**18 + 1))
|
|
451
|
+
assert A.key(atom("none", figure=26)) == A.key(atom("none", figure=26.0))
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
# ── HG-080-02: a unit is one only within its kind ───────────────────────────────────────────────────────────────
|
|
455
|
+
|
|
456
|
+
@pytest.mark.parametrize("a,b,same", [
|
|
457
|
+
("lb", "lbs", True), ("GBP", "sterling", True), ("km", "kilometres", True), ("pounds", "pound", True),
|
|
458
|
+
("widgets", "widgets", True), (None, None, True), (None, "", True),
|
|
459
|
+
("lb", "GBP", False), ("lbs", "gbp", False), ("lb", "sterling", False), # mass is never currency
|
|
460
|
+
("pound", "lb", False), ("pound", "GBP", False), # "pound" could be either
|
|
461
|
+
("km", "miles", False), ("km", None, False),
|
|
462
|
+
])
|
|
463
|
+
def test_units_are_one_only_within_their_kind(a, b, same):
|
|
464
|
+
assert A.same_unit(a, b) is same and A.same_unit(b, a) is same, (a, b)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def test_a_currency_count_cannot_back_a_mass_claim():
|
|
468
|
+
values = {"over": cnt(0, 40, ">", population="shipment", property="quantity", unit="GBP")}
|
|
469
|
+
a = atom("none", ">", 40, population="shipment", property="quantity", unit="lb", n=4)
|
|
470
|
+
v, field, _why = A._decide(a, A.count_fields(values["over"])[0])
|
|
471
|
+
assert (v, field) == ("unresolved", "unit")
|
|
472
|
+
codes = write(values, "No shipment weighs more than 40 lb among the 4 shipments.", [a], ["over"])
|
|
473
|
+
assert "ASSERTION_UNRESOLVED" in codes, codes
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def test_an_ambiguous_unit_says_what_it_could_be():
|
|
477
|
+
v, field, why = A._decide(atom("none", unit="pound", n=4), A.count_fields(cnt(0, unit="lb"))[0])
|
|
478
|
+
assert (v, field) == ("unresolved", "unit") and "could be currency or mass" in why, why
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
# ── an extreme stated as a value is two atoms ───────────────────────────────────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
EXTREME = {"over_26": cnt(0), "at_least_26": cnt(1, op=">=")}
|
|
484
|
+
BOTH = [atom("none", n=4), atom("some", ">=", m=1)]
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def test_an_extreme_stated_as_a_value_needs_both_atoms():
|
|
488
|
+
stmt = "The longest of the 4 routes is 26 km."
|
|
489
|
+
assert write(EXTREME, stmt, BOTH, ["over_26", "at_least_26"]) == []
|
|
490
|
+
for one in (BOTH[:1], BOTH[1:]):
|
|
491
|
+
codes = write(EXTREME, stmt, one, ["over_26", "at_least_26"])
|
|
492
|
+
assert "ASSERTION_MISSING" in codes, (one, codes)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def test_an_extreme_stated_as_a_bound_needs_one_atom():
|
|
496
|
+
values = {"at_least_40": cnt(1, 40, ">=", property="time", unit="minutes")}
|
|
497
|
+
assert write(values, "The slowest of the 4 routes took at least 40 minutes.",
|
|
498
|
+
[atom("some", ">=", 40, property="time", unit="minutes")], ["at_least_40"]) == []
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
@pytest.mark.parametrize("part,needed", [
|
|
502
|
+
("The longest of the 4 routes is 26 km.", 2), ("The highest score was 93.", 2),
|
|
503
|
+
("The longest route, at 26 km, is the only one above 20 km.", 2),
|
|
504
|
+
("The slowest of the 4 routes took at least 40 minutes.", 1), ("The highest of all 4 scores is at most 93.", 1),
|
|
505
|
+
("No route is longer than 26 km.", 1),
|
|
506
|
+
])
|
|
507
|
+
def test_how_many_atoms_a_part_asks_for(part, needed):
|
|
508
|
+
from hypgraph import checks as C
|
|
509
|
+
assert C.atoms_needed(part) == needed
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|