hypgraph 0.7.18__tar.gz → 0.7.20__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.18 → hypgraph-0.7.20}/CHANGELOG.md +40 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/PKG-INFO +1 -1
- {hypgraph-0.7.18 → hypgraph-0.7.20}/docs/PLAN.md +1 -1
- {hypgraph-0.7.18 → hypgraph-0.7.20}/docs/RELEASE-READINESS.md +18 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/docs/verification.md +7 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/__init__.py +1 -1
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/checks.py +312 -1
- hypgraph-0.7.20/src/hypgraph/demos/linked.py +167 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/graph.py +8 -2
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/language.py +13 -2
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/refusals.py +282 -6
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/ui/hypgraph.css +14 -1
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/ui/hypgraph.i18n.js +4 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/ui/hypgraph.js +427 -26
- hypgraph-0.7.20/tests/test_contradictions.py +326 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_engine.py +1 -1
- hypgraph-0.7.20/tests/test_linked_records.py +198 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/.gitignore +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/AGENTS.md +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/CITATION.cff +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/DISCLAIMER.md +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/LICENSE +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/NOTICE +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/README.md +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/docs/decisions/ADR-002-governance-profiles.md +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/docs/profile-workflows-plan.md +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/docs/review-2026-10-24.md +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/docs/site-plan.md +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/docs/spec.md +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/docs/ui-plan.md +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/docs/workflows/README.md +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/docs/workflows/knowledge.md +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/docs/workflows/optimization.md +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/docs/workflows/scientific.md +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/docs/workflows/software.md +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/docs/workflows/trading.md +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/examples/frameworks/README.md +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/pyproject.toml +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/__main__.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/blind.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/bundle.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/cli.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/coord.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/demos/__init__.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/demos/broad_street.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/derive.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/mcp.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/plan.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/policy.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/profiles.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/references.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/report_check.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/schema.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/server.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/skill/SKILL.md +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/stats.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/store.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/ui/__init__.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/ui/atlas.css +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/ui/atlas.js +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/ui/hypgraph.svg +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/ui/index.html +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/ui/vendor/cose-base.js +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/ui/vendor/layout-base.js +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/src/hypgraph/workflow.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_accept_work_kind.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_anchoring.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_blind.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_blind_oracles.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_bound_support.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_bundle_telemetry.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_cancel_releases_lease.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_cited_and_failed_support.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_coord.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_d10.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_engine_fixes.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_framework_examples.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_i18n.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_incremental.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_link_translate.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_log_and_check.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_measure_questions.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_opposed_answers.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_option_lead.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_order_detail.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_profiles.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_profiles_041.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_questions_not_assertions.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_references.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_refusal_friction.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_refusal_results.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_refusals.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_relation_words.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_report_answers.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_rerunnable.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_restates_scale.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_server_auth.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_server_hosts.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_shape_refusals.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_stats_not_backtests.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_stats_values.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_surfaces.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_telemetry_stream.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_trust_boundary.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_ui_app.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_ui_engine.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_uncorrected_effect.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_unnamed_inputs.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_verify_complete.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_verify_lifecycle.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_verify_scope.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_verify_scoped_views.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_verify_trust.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_verify_views.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_versions_not_figures.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_vocabulary_friction.py +0 -0
- {hypgraph-0.7.18 → hypgraph-0.7.20}/tests/test_workflows.py +0 -0
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.20 — 2026-11-01: a claim the record can show false, and who keeps making it
|
|
4
|
+
|
|
5
|
+
- **A claim the record's own counts show false is refused as false** (`COUNT_CONTRADICTS`). "The shortest route is
|
|
6
|
+
31 km" beside a bound count of 2 of 81 routes shorter, or "nothing worth more fits" beside 1 of 1,024 that does, used
|
|
7
|
+
to fail at most as a missing denominator; now the count is named as the contradiction. An extreme claimed of a figure
|
|
8
|
+
no answer reaches is refused too, when the instrument also counts what reaches the figure exactly (0 of n). Not
|
|
9
|
+
claims: a denied extreme ("120 is not the highest"), the domain a stated count ran over ("a count of every route
|
|
10
|
+
finds 1 of 74 shorter") and a stated count itself ("is 1 of 63 routes").
|
|
11
|
+
- **A source cited as support that says the opposite is refused** (`SOURCE_CONTRADICTS`): "the weights are in
|
|
12
|
+
pounds" citing the note that every weight is in kilograms, or casting doubt on what that note asserts without
|
|
13
|
+
denying it ("not independently established", "the only note calling them kilograms"). It no longer counts as a
|
|
14
|
+
line of support for the claim that contradicts it.
|
|
15
|
+
- **Conduct: who corrected and who kept saying it.** `Graph.conduct(inv, author)` reads the refusal log per author:
|
|
16
|
+
claims shown false, corrections, the same claim said again (`reasserted`; a correction that names the old figure to
|
|
17
|
+
deny it is not a repeat), and where each author ends (`still_saying`, `moved_off`, `ends_false`). Refusal rows carry
|
|
18
|
+
the event `seq`, so a claim accepted later that moves off a refused one is seen.
|
|
19
|
+
- `content_words` stems a word and its inflections alike (route / routes, fits / fitting), so a count's name meets
|
|
20
|
+
the sentence it bears on. `language.SUPERLATIVE` is exported. Tests: `tests/test_contradictions.py`.
|
|
21
|
+
|
|
22
|
+
## 0.7.19 — 2026-10-31: linked records, drawn together
|
|
23
|
+
|
|
24
|
+
- **The Explorer draws linked records together.** When an investigation pins nodes of other records (pinned
|
|
25
|
+
references, 0.7.8), its graph now draws each of those records beside it, each in its own box, and follows their
|
|
26
|
+
pins in turn: a parent that pins child 1 and child 2 shows all three, with every pin drawn from the citing source to
|
|
27
|
+
the node it pins. Each record is read as its own bundle with its hash chain re-verified, and each pin is checked
|
|
28
|
+
against the version it names (`checkPin`, held to `verify_reference`'s verdicts by `tests/test_linked_records.py`):
|
|
29
|
+
green when the hash matches, amber when the node was revised since, red when it was withdrawn or doesn't match.
|
|
30
|
+
Beside each pin is the pinned node's standing in its own record. Nothing is merged; the records are laid out in
|
|
31
|
+
columns by distance, each mirrored to keep its pins short. A *Linked records* panel under the graph lists each
|
|
32
|
+
record, its chain, and every pin into it; the overview has a card, and a node of a linked record opens in the
|
|
33
|
+
drawer from its own bundle. Following stops at three links and twelve records by default (`linkDepth`,
|
|
34
|
+
`linkMax`); another site is read only when the viewer asks (`linked: 'same-origin' | 'all' | 'off'`), and a page
|
|
35
|
+
can map record URLs to bundles (`records`, `recordUrl`). `HypGraph.linkRecords`, `checkPin`, `bundleReferences`,
|
|
36
|
+
`normRecord` and `resolveWith` are exported. Translated into es, fr, de and zh-Hans.
|
|
37
|
+
- **A worked example on the site.** `hypgraph.demos.linked` builds three records joined by pins: Broad Street, 1854
|
|
38
|
+
(the demo, unchanged), East London, 1866 (Farr's inquiry, which pins Broad Street's comparison as what it repeats)
|
|
39
|
+
and a synthesis, London's water, 1854–1866, which pins claims in both. `examples/linked/` serves each record as
|
|
40
|
+
its own page and bundle, opening on the graph.
|
|
41
|
+
- The site's `.sub` style no longer leaks into the app's card headings.
|
|
42
|
+
|
|
3
43
|
## 0.7.18 — 2026-10-31: a scoped bundle can't call an active node superseded
|
|
4
44
|
|
|
5
45
|
- **A scoped bundle can't show an active node as superseded and still say its views are checked.** A re-audit of
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hypgraph
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.20
|
|
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.20 (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
|
|
@@ -70,6 +70,24 @@ was taken from a changelog without being checked against the code.*
|
|
|
70
70
|
> **Where that stands.** #77 is merged with green CI (17 of 17 checks); from the 0.7.18 wheel in a fresh venv the
|
|
71
71
|
> suite is 431 passed, and hypswarm (118 passed) and hypmesh (37 passed) pass on it. Released as 0.7.18.
|
|
72
72
|
|
|
73
|
+
> **Linked records (2026-10-31), for 0.7.19.** The Explorer now draws the records an investigation pins beside it,
|
|
74
|
+
> and the records those pin in turn (three links, twelve records at most by default), each in its own box, with every
|
|
75
|
+
> pin drawn to the node it names. Nothing is merged: each record is read as its own bundle with its chain re-verified,
|
|
76
|
+
> and each pin is checked against the version it names by hashing the exact stored body; `checkPin` is held to
|
|
77
|
+
> `verify_reference`'s verdicts. Another site is read only when the viewer asks. The site has a worked example of three
|
|
78
|
+
> linked records in five languages. Tests: `tests/test_linked_records.py`; `dev/ui_smoke.py` 149 of 149 pages.
|
|
79
|
+
>
|
|
80
|
+
> **Where that stands.** #79 is merged with green CI (17 of 17 checks); from the 0.7.19 wheel in a fresh venv the
|
|
81
|
+
> suite is 436 passed, and hypswarm (118 passed) and hypmesh (37 passed) pass on it. Released as 0.7.19.
|
|
82
|
+
|
|
83
|
+
> **Claims the record can show false (2026-11-01), for 0.7.20.** A claim a bound count contradicts is refused as
|
|
84
|
+
> `COUNT_CONTRADICTS` (including an extreme no answer reaches), a claim citing a source that asserts the opposite or
|
|
85
|
+
> doubts it as `SOURCE_CONTRADICTS`, and `Graph.conduct()` tells an author who corrected from one who said it again.
|
|
86
|
+
> Tests: `tests/test_contradictions.py`.
|
|
87
|
+
>
|
|
88
|
+
> **Where that stands.** #81 is merged with green CI (17 of 17 checks); from the 0.7.20 wheel in a fresh venv the
|
|
89
|
+
> suite is 450 passed, and hypswarm (118 passed) and hypmesh (37 passed) pass on it. Released as 0.7.20.
|
|
90
|
+
|
|
73
91
|
## Recommendation: CONDITIONAL GO
|
|
74
92
|
|
|
75
93
|
HypGraph is ready for a public developer preview once #59, #60 and #61 are merged with green CI and released as one
|
|
@@ -107,6 +107,13 @@ separate implementation.
|
|
|
107
107
|
investigation, fails; so does a bundle without a scope that has redacted events.
|
|
108
108
|
- A source that pins another record's node checks that the cited body still hashes to the pinned version. It doesn't
|
|
109
109
|
check the cited record's history: for that, verify that record against its own published head.
|
|
110
|
+
- The Explorer's graph draws the records an investigation pins beside it, and the records those pin in turn (three
|
|
111
|
+
links deep, twelve records at most by default). Each is read as its own bundle and its chain re-verified, and each
|
|
112
|
+
pin is checked against the version it names, hashing the exact stored body as `verify_reference` does. Nothing is
|
|
113
|
+
merged: each record keeps its own standing, shown beside the pin ("checked there", "unchecked there"). A bundle
|
|
114
|
+
can't prove the address it was served from, so a pin ties the citation to the version's hash, not to the URL. The
|
|
115
|
+
page reads another site only when the viewer asks; same-origin records, and records the page maps to a bundle
|
|
116
|
+
(`records` in `HypGraph.mount`), are read at once.
|
|
110
117
|
|
|
111
118
|
## Try it
|
|
112
119
|
|
|
@@ -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.20"
|
|
20
20
|
|
|
21
21
|
from .bundle import anchored, verify_bundle # noqa: E402
|
|
22
22
|
from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
|
|
@@ -16,6 +16,10 @@ and each refusal does the arithmetic the writer skipped, then offers the honest
|
|
|
16
16
|
ABSOLUTE_WITHOUT_COUNT "never" / "always" / "eliminates" / "100%" with no count of 0 or n behind it
|
|
17
17
|
(an absolute over a set the sentence enumerates — "all three subsets: …" — is
|
|
18
18
|
not a generalisation and is left alone)
|
|
19
|
+
COUNT_CONTRADICTS the absolute is contradicted by a bound count of what it's about: k of n with
|
|
20
|
+
0 < k < n, the exceptions it says don't exist were counted. Not a missing count
|
|
21
|
+
(ABSOLUTE_WITHOUT_COUNT) but a false statement, and the refusal log tracks whether
|
|
22
|
+
its author then corrects it or says it again (refusals.conduct)
|
|
19
23
|
ABSOLUTE_UNRELATED_COUNT the 0-of-n it leans on counts something else
|
|
20
24
|
ABSOLUTE_UNDERPOWERED 0 of n with n too small (the rate could still be above 5%), and the statement
|
|
21
25
|
generalises past the n trials instead of saying them ("never leaks")
|
|
@@ -25,6 +29,11 @@ and each refusal does the arithmetic the writer skipped, then offers the honest
|
|
|
25
29
|
SIGNIFICANCE_NOT_SHOWN the test behind it isn't significant
|
|
26
30
|
SIGNIFICANCE_CONTRADICTED "not significant", but the bound evidence is (p < 0.05)
|
|
27
31
|
CAUSAL_WITHOUT_INTERVENTION "causes" / "leads to" when no experiment intervened
|
|
32
|
+
SOURCE_CONTRADICTS the statement denies a word ("…in pounds, not kilograms") that a source it links
|
|
33
|
+
as support asserts, in a sentence with no negation of its own ("every weight is
|
|
34
|
+
in kilograms"): the source says the opposite, so it can't be a line of support.
|
|
35
|
+
(Before this, a claim citing its own refutation was accepted with "3 independent
|
|
36
|
+
lines of support".)
|
|
28
37
|
EXCLUSION_WITHOUT_POWER "no effect" / "rules out" without a search shown to find a planted effect
|
|
29
38
|
CONVENTION_HEDGED a convention that hedges
|
|
30
39
|
VALUE_NOT_A_NUMBER (results) a value's k or n is not a number: {"k": "40/50", "n": "rank 1 of 20"}
|
|
@@ -248,12 +257,19 @@ def _absolutes(stmt: str, scope: str, body: dict, bound: list) -> list[Problem]:
|
|
|
248
257
|
words = [w for w in L.asserted(L.ABSOLUTE, stmt) if not _enumerated(stmt, w)]
|
|
249
258
|
if any(str(m.get("key") or "").lower() in ("gap", "bound") for m, _rv in bound):
|
|
250
259
|
words = [w for w in words if not _PROVEN.match(w)] # an optimum with its bound (gap 0) is proven
|
|
260
|
+
words = [w for w in words if not _names_counted_set(stmt, w, bound)]
|
|
251
261
|
if not words:
|
|
252
|
-
return
|
|
262
|
+
return _superlative_contradicted(stmt, bound)
|
|
253
263
|
w = words[0]
|
|
254
264
|
arms = [(m, rv, _arm(rv)) for m, rv in bound if _arm(rv)]
|
|
255
265
|
exact = [(m, rv, a) for m, rv, a in arms if a[0] in (0, a[1])]
|
|
256
266
|
if not exact:
|
|
267
|
+
about = L.content_words(stmt)
|
|
268
|
+
against = [(m, a) for m, _rv, a in arms if 0 < a[0] < a[1]
|
|
269
|
+
and L.content_words(f"{m.get('name', '')} {m.get('key', '')}") & about]
|
|
270
|
+
if against:
|
|
271
|
+
m, (k, n) = max(against, key=lambda x: x[1][1])
|
|
272
|
+
return [_count_contradicts(stmt, w, words, m, k, n)]
|
|
257
273
|
seen = ", ".join(f"{a[0]} of {a[1]:,}" for _m, _rv, a in arms)
|
|
258
274
|
return [Problem("ABSOLUTE_WITHOUT_COUNT",
|
|
259
275
|
f"'{w}' is absolute: back it with a k/n measurement of what it's about, where k is 0 or n, and "
|
|
@@ -295,6 +311,298 @@ def _absolutes(stmt: str, scope: str, body: dict, bound: list) -> list[Problem]:
|
|
|
295
311
|
fix=f"add 'in {n:,} trials'", words=words)]
|
|
296
312
|
|
|
297
313
|
|
|
314
|
+
_STATED_COUNT = re.compile(r"\b(\d[\d,]*) of (\d[\d,]*)\b")
|
|
315
|
+
_CLAUSE = re.compile(r"[;!?]|\.(?!\d)")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _names_counted_set(stmt: str, w: str, bound: list) -> bool:
|
|
319
|
+
"""Is the absolute the set a count the statement itself reports ran over: "a count of every possible selection
|
|
320
|
+
finds 2 of 1,024 that are worth more"? It names what was counted, not a claim that nothing lies outside it. (Read
|
|
321
|
+
as an absolute, the record refused that honest correction as the lie said again.)"""
|
|
322
|
+
return _names_stated_count(stmt, w, {a for _m, rv in bound if (a := _arm(rv))})
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _names_stated_count(stmt: str, w: str, counts: set | None = None) -> bool:
|
|
326
|
+
"""Does ``w``'s clause state a k-of-n count (one of ``counts``, when given)?"""
|
|
327
|
+
for c in _CLAUSE.split(stmt):
|
|
328
|
+
if re.search(r"\b" + re.escape(w) + r"\b", c, re.I) and any(
|
|
329
|
+
counts is None or (int(a.replace(",", "")), int(b.replace(",", ""))) in counts
|
|
330
|
+
for a, b in _STATED_COUNT.findall(c)):
|
|
331
|
+
return True
|
|
332
|
+
return False
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def claimed_extremes(stmt: str) -> list[str]:
|
|
336
|
+
"""What a sentence claims as an extreme: its asserted superlatives (``_extremes``) and absolutes, less an absolute
|
|
337
|
+
that names the set a count in its own clause ran over ("a count of every route finds 1 of 74 shorter"). The refusal
|
|
338
|
+
log reads a later write with this to tell a correction from the lie said again."""
|
|
339
|
+
return _extremes(stmt) + [w for w in L.asserted(L.ABSOLUTE, stmt)
|
|
340
|
+
if not _enumerated(stmt, w) and not _names_stated_count(stmt, w)]
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# the extreme's own predicate denied after it, in its clause: "the highest total value ... is not 130"
|
|
344
|
+
_DENIED_EXTREME = re.compile(r"\b(?:is|are|was|were)\s+(?:not|no longer|never)\b|\b(?:isn't|aren't|wasn't|weren't)\b|"
|
|
345
|
+
# or said to lie beyond a figure: "the true maximum is higher than 98"
|
|
346
|
+
r"\b(?:is|are|was|were|lies)\s+(?:\w+\s+)?(?:higher|lower|greater|larger|smaller|more|less|"
|
|
347
|
+
r"above|below|over|under|shorter|longer|beyond)\b", re.I)
|
|
348
|
+
# or denied before it: "120 is not the highest total value", "isn't the shortest"
|
|
349
|
+
_DENIED_BEFORE = re.compile(r"(?:\b(?:is|are|was|were)\s+(?:not|no longer|never)|\b(?:isn't|aren't|wasn't|weren't))"
|
|
350
|
+
r"\s+(?:the\s+|a\s+|an\s+)?(?:true\s+|real\s+|actual\s+)?$", re.I)
|
|
351
|
+
_COPULA = re.compile(r"\b(?:is|are|was|were|isn't|aren't|wasn't|weren't|lies)\b", re.I)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _denied(after: str) -> bool:
|
|
355
|
+
"""Does the extreme's own predicate — the first copula after it — deny it or put it beyond a figure? Only that
|
|
356
|
+
one: "the highest value is 98 ..., so the true maximum is higher" still claims 98."""
|
|
357
|
+
cop = _COPULA.search(after)
|
|
358
|
+
return bool(cop and _DENIED_EXTREME.match(after, cop.start()))
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# a count named for what lies beyond a figure ("fitting selections worth more", "shorter routes connecting") is about
|
|
362
|
+
# any extreme the statement claims, whatever words the statement uses for it ("the highest total value is 99")
|
|
363
|
+
_BEYOND = re.compile(r"\b(?:more|less|fewer|greater|higher|lower|better|worse|shorter|longer|cheaper|faster|slower|"
|
|
364
|
+
r"larger|smaller|above|below|exceed\w*|beat\w*)\b", re.I)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _extremes(stmt: str) -> list[str]:
|
|
368
|
+
"""The superlatives a statement asserts of a figure. Not one whose clause goes on to deny it: "the highest total
|
|
369
|
+
value that fits is not 130" corrects the extreme, it doesn't claim one. Nor one whose clause names nothing after
|
|
370
|
+
it ("the shortest route must be computed from the figures as kilometres"): no count can contradict an extreme of
|
|
371
|
+
nothing in particular. (The record used to refuse both as false and log corrections as the lie said again.)"""
|
|
372
|
+
out = []
|
|
373
|
+
for w in L.asserted(L.SUPERLATIVE, stmt):
|
|
374
|
+
for m in re.finditer(r"\b" + re.escape(w) + r"\b", stmt, re.I):
|
|
375
|
+
end = next((x.start() for x in _CLAUSE.finditer(stmt, m.end())), len(stmt))
|
|
376
|
+
start = max((x.end() for x in _CLAUSE.finditer(stmt, 0, m.start())), default=0)
|
|
377
|
+
if (not _denied(stmt[m.end():end]) and not _DENIED_BEFORE.search(stmt[start:m.start()])
|
|
378
|
+
and re.search(r"\d", stmt[start:end])):
|
|
379
|
+
out.append(w)
|
|
380
|
+
break
|
|
381
|
+
return out
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _count_contradicts(stmt: str, w: str, words: list, m: dict, k: int, n: int) -> Problem:
|
|
385
|
+
"""The refusal for a statement the record's own count shows false. It names what is false in the statement's own
|
|
386
|
+
terms: a count it states that the record's differs from, or the extreme it claims ("highest"), before the word
|
|
387
|
+
that triggered the check."""
|
|
388
|
+
said = [int(a.replace(",", "")) for a, b in _STATED_COUNT.findall(stmt)
|
|
389
|
+
if int(b.replace(",", "")) == n and int(a.replace(",", "")) != k]
|
|
390
|
+
sup = _extremes(stmt)
|
|
391
|
+
head = (f"the statement says {said[0]:,} of {n:,}, but the record's own count is {k:,} of {n:,}" if said
|
|
392
|
+
else f"'{sup[0] if sup else w}' is contradicted by the record's own count: {k:,} of {n:,}")
|
|
393
|
+
return Problem("COUNT_CONTRADICTS",
|
|
394
|
+
f"{head} ({m.get('name') or m.get('key')}). The exceptions the statement rules out were "
|
|
395
|
+
f"counted, so it is false as written: correct it, or say '{k:,} of {n:,}'",
|
|
396
|
+
fix=f"correct the statement, or say '{k:,} of {n:,}'",
|
|
397
|
+
words=words + [f"={_fmt(v)}" for v in _subject_numbers(stmt, k, n)])
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _qualified(stmt: str, k: int, n: int) -> bool:
|
|
401
|
+
"""Does the statement give this count about a figure of its own ("1 of 61 routes are shorter than 26 km")?
|
|
402
|
+
Then it's a count of something beyond that figure, not of exceptions to the extreme it claims."""
|
|
403
|
+
forms = {f"{k} of {n}", f"{k:,} of {n:,}"}
|
|
404
|
+
return any(any(f in c for f in forms) and re.search(r"\bthan\b", c) for c in _CLAUSE.split(stmt))
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _superlative_contradicted(stmt: str, bound: list) -> list[Problem]:
|
|
408
|
+
"""An extreme ("the shortest route is 34 km") with no absolute word, where the record's own count of what lies
|
|
409
|
+
beyond it is neither 0 nor all and no exact count backs any figure: refused as false (COUNT_CONTRADICTS)."""
|
|
410
|
+
sup = _extremes(stmt)
|
|
411
|
+
if not sup:
|
|
412
|
+
return []
|
|
413
|
+
about = L.content_words(stmt)
|
|
414
|
+
rel = [(m, a) for m, rv in bound if (a := _arm(rv))
|
|
415
|
+
and (L.content_words(f"{m.get('name', '')} {m.get('key', '')}") & about
|
|
416
|
+
or _BEYOND.search(f"{m.get('name', '')} {m.get('key', '')}".replace("_", " ")))]
|
|
417
|
+
if not rel or any(a[0] in (0, a[1]) for _m, a in rel):
|
|
418
|
+
return []
|
|
419
|
+
against = [(m, a) for m, a in rel if 0 < a[0] < a[1] and not _qualified(stmt, *a)]
|
|
420
|
+
if not against:
|
|
421
|
+
return []
|
|
422
|
+
m, (k, n) = max(against, key=lambda x: x[1][1])
|
|
423
|
+
return [_count_contradicts(stmt, sup[0], sup, m, k, n)]
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
# a count of what reaches a figure exactly: "fitting selections worth exactly 108", "routes exactly 33 km long"
|
|
427
|
+
_REACHES = re.compile(r"\bexactly\s+(-?\d[\d,]*(?:\.\d+)?)", re.I)
|
|
428
|
+
_FIGURE = re.compile(r"\s*(?:[A-Za-z$]+\s+){0,2}?\$?(-?\d[\d,]*(?:\.\d+)?)(?![\d.,]?\d)(?!\s*of\s+\d)")
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _extreme_figures(stmt: str) -> set[float]:
|
|
432
|
+
"""The figures a statement's asserted extremes name: the number its own predicate gives ("the highest total value
|
|
433
|
+
that fits within 36 kg is 97" names 97, not 36), else every number in the extreme's clause. A figure elsewhere in
|
|
434
|
+
the sentence ("..., not 97") is not one, nor a stated count ("the shortest route is 1 of 63 routes shorter")."""
|
|
435
|
+
out = set()
|
|
436
|
+
for w in _extremes(stmt):
|
|
437
|
+
for m in re.finditer(r"\b" + re.escape(w) + r"\b", stmt, re.I):
|
|
438
|
+
end = next((x.start() for x in _CLAUSE.finditer(stmt, m.end())), len(stmt))
|
|
439
|
+
start = max((x.end() for x in _CLAUSE.finditer(stmt, 0, m.start())), default=0)
|
|
440
|
+
after = stmt[m.end():end]
|
|
441
|
+
if _denied(after) or _DENIED_BEFORE.search(stmt[start:m.start()]):
|
|
442
|
+
continue
|
|
443
|
+
cop = _COPULA.search(after)
|
|
444
|
+
fig = _FIGURE.match(after, cop.end()) if cop else None
|
|
445
|
+
if fig:
|
|
446
|
+
out.add(float(fig.group(1).replace(",", "")))
|
|
447
|
+
elif not (cop and re.match(r"\s*(?:\w+\s+){0,2}?-?\d[\d,]*\s+of\s+\d", after[cop.end():])):
|
|
448
|
+
out |= {v for v, *_ in numbers(stmt[start:end])}
|
|
449
|
+
return out
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _unreached(stmt: str, bound: list) -> list[Problem]:
|
|
453
|
+
"""An extreme claimed of a figure nothing reaches. The count of what lies beyond the figure can't show it false
|
|
454
|
+
(nothing beats a figure above the true best), so the instrument also counts what reaches it exactly, naming the
|
|
455
|
+
figure ("fitting selections worth exactly 140"); 0 of n there means no answer is that figure, so it is no extreme.
|
|
456
|
+
(Without it, "the highest value is 140", with an item counted twice against a true best of 113, was accepted.)"""
|
|
457
|
+
sup = _extremes(stmt)
|
|
458
|
+
figs = _extreme_figures(stmt) if sup else set()
|
|
459
|
+
for m, rv in bound:
|
|
460
|
+
a = _arm(rv)
|
|
461
|
+
hit = _REACHES.search(f"{m.get('name', '')}")
|
|
462
|
+
if not a or a[0] != 0 or not hit:
|
|
463
|
+
continue
|
|
464
|
+
x = float(hit.group(1).replace(",", ""))
|
|
465
|
+
if x not in figs:
|
|
466
|
+
continue
|
|
467
|
+
name = m.get("name") or m.get("key")
|
|
468
|
+
return [Problem("COUNT_CONTRADICTS",
|
|
469
|
+
f"'{sup[0]}' names {_fmt(x)}, but the record's own count finds 0 of {a[1]:,} {name}: no "
|
|
470
|
+
f"answer reaches {_fmt(x)}, so it can't be the {sup[0]}. Correct the figure, or say what was "
|
|
471
|
+
f"reached",
|
|
472
|
+
fix="correct the figure to one that is reached", words=sup + [f"={_fmt(x)}"])]
|
|
473
|
+
return []
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _fmt(v: float) -> str:
|
|
477
|
+
return str(int(v)) if float(v).is_integer() else str(v)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _subject_numbers(stmt: str, k: int, n: int) -> list[float]:
|
|
481
|
+
"""The figures a contradicted statement asserts, other than the count itself: what it would have to change to be
|
|
482
|
+
corrected (refusals.conduct reads them to tell a correction from the same claim said again)."""
|
|
483
|
+
return [v for v, *_ in numbers(stmt) if v not in (0, k, n)]
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
# a word a statement denies: "not kilograms", "doesn't apply", "never reached" (the one or two words after it)
|
|
487
|
+
_DENIAL = re.compile(r"(?:\bnot\b|\bnever\b|n't\b|\brather than\b|\binstead of\b)\s+((?:[A-Za-z]+\s+)?[A-Za-z]+)",
|
|
488
|
+
re.I)
|
|
489
|
+
_PASSAGE_NEGATION = re.compile(r"\b(not|no|never|none|nor|neither|without)\b|n't\b", re.I)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def denied_words(stmt: str) -> set[str]:
|
|
493
|
+
"""Content words (stemmed, as language.content_words) that the statement explicitly denies."""
|
|
494
|
+
out: set[str] = set()
|
|
495
|
+
for m in _DENIAL.finditer(stmt or ""):
|
|
496
|
+
out |= L.content_words(m.group(1))
|
|
497
|
+
return out
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
# casting doubt on a word rather than denying it: "is not independently established that they are kilograms", "the
|
|
501
|
+
# only note calling them kilometres", "conflicts with any pound reading" (a party can cite a note as support and doubt
|
|
502
|
+
# it in these words, which SOURCE_CONTRADICTS' denial check doesn't see)
|
|
503
|
+
_DOUBT = re.compile(
|
|
504
|
+
r"\bnot\s+(?:\w+ly\s+)?(?:established|confirmed|verified|proven|shown|settled|resolved|reliabl\w*|determined)\b"
|
|
505
|
+
r"|\b(?:un(?:confirmed|verified|established|proven|resolved|reliable|corroborated)|disputed|questionable|doubtful)\b"
|
|
506
|
+
r"|\b(?:cannot|can't|can not)\s+be\s+(?:relied|trusted|applied|used|taken|confirmed|verified|established|"
|
|
507
|
+
r"certified|determined)\b"
|
|
508
|
+
r"|\b(?:does|do|did)\s+not\s+(?:establish|confirm|determine|prove|settle|support)\b"
|
|
509
|
+
r"|\b(?:doesn't|don't|didn't)\s+(?:establish|confirm|determine|prove|settle|support)\b"
|
|
510
|
+
r"|\b(?:may|might|could)\s+(?:be|have been)\s+(?:recorded|given|stated|measured|in)\b|\bno\s+independent\b"
|
|
511
|
+
r"|\bonly\s+(?:\w+\s+)?(?:thing|evidence|source|note|basis|assertion|claim|label)\b|\bat issue\b"
|
|
512
|
+
r"|\bin (?:doubt|dispute|question)\b|\bconflicts?\s+with\b|\bcontradicted\s+by\b|\binconsistent\b", re.I)
|
|
513
|
+
_DOUBT_CLAUSE = re.compile(r"([,;:()]|\b(?:because|while|but|although|though|whereas|since|so|and)\b)", re.I)
|
|
514
|
+
# a doubt after these turns on the clause before it: "E12 says kilometres, while the figures may be in miles"
|
|
515
|
+
_TURNS_BACK = re.compile(r"\s*(?:while|but|although|though|whereas)\s*$", re.I)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def doubted_words(stmt: str) -> set[str]:
|
|
519
|
+
"""Content words (stemmed) that the statement casts doubt on without denying them: those of a clause with a doubt
|
|
520
|
+
in it, and of the clause before when the doubt turns back on it ("E12 says kilometres, while the figures may be
|
|
521
|
+
in miles"; "the note says kilograms, which conflicts with any pound reading")."""
|
|
522
|
+
parts = _DOUBT_CLAUSE.split(stmt or "")
|
|
523
|
+
clauses, seps = parts[0::2], [""] + parts[1::2]
|
|
524
|
+
out: set[str] = set()
|
|
525
|
+
for i, c in enumerate(clauses):
|
|
526
|
+
if not c or not _DOUBT.search(c):
|
|
527
|
+
continue
|
|
528
|
+
out |= L.content_words(c)
|
|
529
|
+
if i and (_TURNS_BACK.match(seps[i]) or re.match(r"\s*(?:which|that)\b", c, re.I)):
|
|
530
|
+
back = i - 1
|
|
531
|
+
while back > 0 and not clauses[back].strip():
|
|
532
|
+
back -= 1
|
|
533
|
+
out |= L.content_words(clauses[back])
|
|
534
|
+
return out
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
_ASSERTS = re.compile(r"\b(?:is|are|was|were)\b", re.I)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _asserted_words(sent: str) -> list[str]:
|
|
541
|
+
"""What a source sentence asserts, as stems in order: the words after its last copula ("every weight in the
|
|
542
|
+
inventory is in kilograms" asserts kilograms). A line with no copula ("item 2, weight 3 kg, value 26") is data,
|
|
543
|
+
not an assertion a doubt could turn on: a doubt about "the highest value" isn't one about that line's value."""
|
|
544
|
+
cop = list(_ASSERTS.finditer(sent))
|
|
545
|
+
if not cop:
|
|
546
|
+
return []
|
|
547
|
+
return [w for t in re.findall(r"[A-Za-z]+", sent[cop[-1].end():]) for w in L.content_words(t)]
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _affirmed_in(passage: str, stem: str) -> str:
|
|
551
|
+
"""The sentence of ``passage`` that asserts a word with this stem without negating anything, or ''."""
|
|
552
|
+
for sent in re.split(r"(?<=[.!?;])\s+", passage or ""):
|
|
553
|
+
if stem in L.content_words(sent) and not _PASSAGE_NEGATION.search(sent):
|
|
554
|
+
return sent.strip()
|
|
555
|
+
return ""
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _doubted_in(passage: str, doubted: set[str]) -> tuple[list[str], str] | None:
|
|
559
|
+
"""The words a plain (un-negated) sentence of ``passage`` asserts ("every weight ... is in kilograms") that the
|
|
560
|
+
statement doubts, and the sentence. Doubting a source's subject ("whether the container is available") is not
|
|
561
|
+
doubting what it says of it ("the container takes at most 36 kg")."""
|
|
562
|
+
if not doubted:
|
|
563
|
+
return None
|
|
564
|
+
for sent in re.split(r"(?<=[.!?;])\s+", passage or ""):
|
|
565
|
+
if _PASSAGE_NEGATION.search(sent):
|
|
566
|
+
continue
|
|
567
|
+
hit = list(dict.fromkeys(w for w in _asserted_words(sent) if w in doubted))
|
|
568
|
+
if hit:
|
|
569
|
+
return hit, sent.strip()
|
|
570
|
+
return None
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _source_contradicts(s: Snap, stmt: str, support: list[str]) -> list[Problem]:
|
|
574
|
+
denied, doubted = denied_words(stmt), doubted_words(stmt)
|
|
575
|
+
if not denied and not doubted:
|
|
576
|
+
return []
|
|
577
|
+
for src in support:
|
|
578
|
+
n = s.nodes.get(src)
|
|
579
|
+
if not n or n["type"] != "source":
|
|
580
|
+
continue
|
|
581
|
+
passage = str(n["body"].get("passage") or "")
|
|
582
|
+
doubt = _doubted_in(passage, doubted)
|
|
583
|
+
if doubt:
|
|
584
|
+
stems, sent = doubt
|
|
585
|
+
cite = str(n["body"].get("citation") or "").strip()
|
|
586
|
+
label = cite[:80] if cite else (n.get("code") or src)
|
|
587
|
+
return [Problem("SOURCE_CONTRADICTS",
|
|
588
|
+
f"the statement casts doubt on '{stems[-1]}', which a source it links as support ({label}) "
|
|
589
|
+
f"asserts: \"{sent[:200]}\". A source can't support a doubt about what it says: accept it, "
|
|
590
|
+
"or link it as a challenge and cite a source that says otherwise",
|
|
591
|
+
fix="accept what the source says, or link it as a challenge", words=stems)]
|
|
592
|
+
for stem in sorted(denied):
|
|
593
|
+
sent = _affirmed_in(passage, stem)
|
|
594
|
+
if sent:
|
|
595
|
+
cite = str(n["body"].get("citation") or "").strip()
|
|
596
|
+
label = cite[:80] if cite else (n.get("code") or src)
|
|
597
|
+
return [Problem("SOURCE_CONTRADICTS",
|
|
598
|
+
f"the statement denies '{stem}', but a source it links as support ({label}) says: "
|
|
599
|
+
f"\"{sent[:200]}\". A source that "
|
|
600
|
+
"says the opposite isn't support: correct the statement, or link the source as a "
|
|
601
|
+
"challenge",
|
|
602
|
+
fix="correct the statement, or link that source as a challenge", words=[stem])]
|
|
603
|
+
return []
|
|
604
|
+
|
|
605
|
+
|
|
298
606
|
def _measured_claim(s: Snap, support: list[str]) -> bool:
|
|
299
607
|
"""A supporting claim that itself binds two sides: a comparison built on it inherits them."""
|
|
300
608
|
for sid in support:
|
|
@@ -636,9 +944,12 @@ def claim_issues(s: Snap, body: dict, support: list[str], inv: str) -> list[Prob
|
|
|
636
944
|
+ ", ".join(named[:6]) + " — write the number in the sentence; it's bound to that key, "
|
|
637
945
|
"so the record checks it", fix="replace each key with its number", words=named[:6]))
|
|
638
946
|
problems += _absolutes(clean, scope, body, bound)
|
|
947
|
+
if not any(p.code == "COUNT_CONTRADICTS" for p in problems):
|
|
948
|
+
problems += _unreached(clean, bound)
|
|
639
949
|
if kind not in ("lead", "convention"):
|
|
640
950
|
problems += _comparisons(s, clean, bound, support)
|
|
641
951
|
problems += _causal(s, clean, support)
|
|
952
|
+
problems += _source_contradicts(s, clean, support)
|
|
642
953
|
problems += _exclusion(s, clean, kind, support, bound)
|
|
643
954
|
if kind == "finding":
|
|
644
955
|
problems += _uncorrected(s, clean, bound, inv)
|