hypgraph 0.7.20__tar.gz → 0.7.22__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.20 → hypgraph-0.7.22}/CHANGELOG.md +52 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/PKG-INFO +1 -1
- {hypgraph-0.7.20 → hypgraph-0.7.22}/docs/PLAN.md +1 -1
- {hypgraph-0.7.20 → hypgraph-0.7.22}/docs/RELEASE-READINESS.md +26 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/__init__.py +1 -1
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/checks.py +226 -18
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/refusals.py +28 -13
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_contradictions.py +261 -1
- {hypgraph-0.7.20 → hypgraph-0.7.22}/.gitignore +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/AGENTS.md +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/CITATION.cff +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/DISCLAIMER.md +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/LICENSE +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/NOTICE +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/README.md +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/docs/decisions/ADR-002-governance-profiles.md +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/docs/profile-workflows-plan.md +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/docs/review-2026-10-24.md +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/docs/site-plan.md +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/docs/spec.md +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/docs/ui-plan.md +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/docs/verification.md +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/docs/workflows/README.md +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/docs/workflows/knowledge.md +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/docs/workflows/optimization.md +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/docs/workflows/scientific.md +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/docs/workflows/software.md +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/docs/workflows/trading.md +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/examples/frameworks/README.md +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/pyproject.toml +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/__main__.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/blind.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/bundle.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/cli.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/coord.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/demos/__init__.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/demos/broad_street.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/demos/linked.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/derive.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/graph.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/language.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/mcp.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/plan.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/policy.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/profiles.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/references.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/report_check.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/schema.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/server.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/skill/SKILL.md +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/stats.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/store.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/ui/__init__.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/ui/atlas.css +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/ui/atlas.js +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/ui/hypgraph.css +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/ui/hypgraph.i18n.js +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/ui/hypgraph.js +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/ui/hypgraph.svg +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/ui/index.html +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/ui/vendor/cose-base.js +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/ui/vendor/layout-base.js +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/src/hypgraph/workflow.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_accept_work_kind.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_anchoring.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_blind.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_blind_oracles.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_bound_support.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_bundle_telemetry.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_cancel_releases_lease.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_cited_and_failed_support.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_coord.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_d10.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_engine.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_engine_fixes.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_framework_examples.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_i18n.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_incremental.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_link_translate.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_linked_records.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_log_and_check.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_measure_questions.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_opposed_answers.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_option_lead.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_order_detail.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_profiles.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_profiles_041.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_questions_not_assertions.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_references.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_refusal_friction.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_refusal_results.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_refusals.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_relation_words.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_report_answers.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_rerunnable.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_restates_scale.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_server_auth.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_server_hosts.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_shape_refusals.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_stats_not_backtests.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_stats_values.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_surfaces.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_telemetry_stream.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_trust_boundary.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_ui_app.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_ui_engine.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_uncorrected_effect.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_unnamed_inputs.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_verify_complete.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_verify_lifecycle.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_verify_scope.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_verify_scoped_views.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_verify_trust.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_verify_views.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_versions_not_figures.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_vocabulary_friction.py +0 -0
- {hypgraph-0.7.20 → hypgraph-0.7.22}/tests/test_workflows.py +0 -0
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.22 — 2026-11-01: a label can't move a count, every part of a claim is checked, units rule each other out
|
|
4
|
+
|
|
5
|
+
Fixes from an independent audit of 0.7.21 (HG-021-01 to -04); each case below failed on 0.7.21 and is now a test.
|
|
6
|
+
|
|
7
|
+
- **The result says what a count is about, not its label** (HG-021-01). 0.7.21 read a count's figure from its
|
|
8
|
+
free-text name, so renaming the count of 2 beyond 93 "worth more than 140" moved it off "the highest value is 93",
|
|
9
|
+
and the false claim went in. An instrument can now state the figure with the count
|
|
10
|
+
(`{"k": 2, "n": 1024, "figure": 93}`); that figure is the only one, a measurement whose name says another is refused
|
|
11
|
+
as `MEASUREMENT_MISMATCH`, and the figure accounts for the number in the statement. Without one, a name that
|
|
12
|
+
disagrees with its key adds a figure and never replaces one: a count that finds exceptions refutes a claim at either
|
|
13
|
+
figure, and a count of none vouches only when both are the claim's.
|
|
14
|
+
- **Every absolute and every extreme in a claim is checked** (HG-021-02). A true "nothing worth more than 140 fits
|
|
15
|
+
among the 1,024 selections" ended the check, and "the highest value is 93" beside it was never looked at. One check
|
|
16
|
+
now runs per figure the absolutes are about, and the extreme is checked as well.
|
|
17
|
+
- **A source that states another unit of the same kind isn't support** (HG-021-03). Only a claim that denied or
|
|
18
|
+
doubted the source's word was compared with it, so "the recorded weights are in pounds", citing the note that every
|
|
19
|
+
recorded weight is in kilograms, went in. Units of mass, length, temperature and volume, and currencies, now rule
|
|
20
|
+
each other out when the claim and the source say them of the same thing (`SOURCE_CONTRADICTS`). A clause that only
|
|
21
|
+
converts ("in kilograms, about 2.2 pounds each") or states a condition ("if the weights are pounds") asserts
|
|
22
|
+
nothing. `conduct()` counts saying the ruled-out unit again as saying the claim again.
|
|
23
|
+
- **`shown_false` is not a lying rate** (HG-021-04). The `conduct()` docstring now says so: it adds up contradictions
|
|
24
|
+
by the record's own count and by a cited source, and a source can be wrong. Read `by_count`, `by_source`,
|
|
25
|
+
`corrected`, `reasserted` and `still_saying` instead.
|
|
26
|
+
- Limits, unchanged: contradictions in meaning outside the unit families aren't checked; a claim that drops the
|
|
27
|
+
citation or count that would contradict it is not checked against it; `strict_claims: false` runs no claim checks;
|
|
28
|
+
and a result without a stated figure still relies on its key to say what it counted.
|
|
29
|
+
|
|
30
|
+
## 0.7.21 — 2026-11-01: counts at the claim's figure, scoped negation, conduct by kind
|
|
31
|
+
|
|
32
|
+
Fixes from an independent audit of 0.7.20 (HG-020-01 to -04); each case below failed on 0.7.20 and is now a test.
|
|
33
|
+
|
|
34
|
+
- **A count bears on a claim only at the figure the claim names.** A count says what lies beyond one figure
|
|
35
|
+
("fitting selections worth more than 93"). In 0.7.20, binding a count of nothing beyond 140 beside the count of 2
|
|
36
|
+
beyond 93, in either order, let "nothing worth more than 93 fits" and "the highest value is 93" in (HG-020-01); and
|
|
37
|
+
a count beyond 93 refused the true "the highest value is 95" as `COUNT_CONTRADICTS`, which `conduct()` then held
|
|
38
|
+
against its author (HG-020-02). Now a count of exceptions at the claim's own figure refutes it whatever else is bound
|
|
39
|
+
beside it, and a count at another figure neither refutes nor rescues it. A count says its figure in its name
|
|
40
|
+
("more than 93", "above 93"); a number at the end of a key counts only when it is a figure the claim names, since it
|
|
41
|
+
may just number repeated counts (`worth_more_2` is the second count, not one beyond 2). A count that names no figure
|
|
42
|
+
is read as before.
|
|
43
|
+
- **A source's "not X" negates X, not its whole sentence.** "Every recorded weight is in kilograms, not pounds" used
|
|
44
|
+
to be skipped for its "not", so it supported a claim of pounds (HG-020-03). A negation now reaches to the end of its
|
|
45
|
+
clause; "the weights were not recorded in kilograms" still asserts nothing about kilograms.
|
|
46
|
+
- **Conduct says which contradiction it was, and describes behaviour, not intent** (HG-020-04). `conduct()` adds
|
|
47
|
+
`by_count` (the record's own count contradicted the claim) and `by_source` (a source the author cited as support
|
|
48
|
+
says otherwise: the citation doesn't support the claim, which is not proof it is false, since a source can be
|
|
49
|
+
wrong). `shown_false` is still their total. Its sentence names each kind, and a last word that was refused now
|
|
50
|
+
reads "contradicted by the record". The docstring no longer reads intent into it: conduct is evidence for a
|
|
51
|
+
reviewer, not a verdict on anyone.
|
|
52
|
+
- Limits, unchanged and now stated: a claim that drops the citation or count that would contradict it is not checked
|
|
53
|
+
against it, and an investigation with `strict_claims: false` runs no claim checks.
|
|
54
|
+
|
|
3
55
|
## 0.7.20 — 2026-11-01: a claim the record can show false, and who keeps making it
|
|
4
56
|
|
|
5
57
|
- **A claim the record's own counts show false is refused as false** (`COUNT_CONTRADICTS`). "The shortest route is
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hypgraph
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.22
|
|
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.22 (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
|
|
@@ -88,6 +88,32 @@ was taken from a changelog without being checked against the code.*
|
|
|
88
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
89
|
> suite is 450 passed, and hypswarm (118 passed) and hypmesh (37 passed) pass on it. Released as 0.7.20.
|
|
90
90
|
|
|
91
|
+
> **Audit of 0.7.20 (2026-11-01), fixed for 0.7.21.** An independent audit found three false verdicts and a design
|
|
92
|
+
> point, each reproduced on the 0.7.20 wheel: a zero count at another figure cancelled the count that refutes a claim
|
|
93
|
+
> (HG-020-01), a count at another figure refuted a true extreme (HG-020-02), a "not" anywhere in a source sentence hid
|
|
94
|
+
> what it asserts (HG-020-03), and conduct counted a claim that goes against its own citation as shown false and read
|
|
95
|
+
> intent into it (HG-020-04). Counts now bear only at the figure the claim names, a negation reaches to the end of its
|
|
96
|
+
> clause, and conduct reports `by_count` / `by_source` and describes behaviour, not intent. Each case is a regression
|
|
97
|
+
> test written to fail on 0.7.20. Replaying 360 recorded adversarial trials gives the same standings as 0.7.20.
|
|
98
|
+
>
|
|
99
|
+
> **Where that stands.** #83 is merged with green CI (17 of 17 checks); from the 0.7.21 wheel in a fresh venv the
|
|
100
|
+
> suite is 466 passed, and hypswarm (118 passed) and hypmesh (37 passed) pass on it. Released as 0.7.21.
|
|
101
|
+
|
|
102
|
+
> **Audit of 0.7.21 (2026-11-01), fixed for 0.7.22.** A second independent audit found three ways a false claim still
|
|
103
|
+
> got in, each reproduced on the 0.7.21 wheel, and a design point: renaming the count of 2 beyond 93 "worth more than
|
|
104
|
+
> 140" moved it off "the highest value is 93" (HG-021-01); a true "nothing worth more than 140 fits" beside that false
|
|
105
|
+
> extreme ended the check (HG-021-02); and "the recorded weights are in pounds", citing the note that they are in
|
|
106
|
+
> kilograms, was never compared with it because it denied nothing (HG-021-03). `shown_false` was open to being read as
|
|
107
|
+
> a lying rate (HG-021-04). A result can now state a count's figure, and a label can only widen what a count refutes;
|
|
108
|
+
> every absolute and extreme in a claim is checked; units of one kind rule each other out against a cited source; the
|
|
109
|
+
> conduct docstring says `shown_false` is not a lying rate. Each case is a regression test written to fail on 0.7.21.
|
|
110
|
+
> Replaying 360 recorded adversarial trials (1,900 entries): 5 entries change, all the units liar's; no honest or
|
|
111
|
+
> mistaken party's entry changes; two more parties still saying the false unit are named and no one new is misnamed.
|
|
112
|
+
>
|
|
113
|
+
> **Where that stands.** #85 is merged with green CI (17 of 17 checks); from the 0.7.22 wheel in a fresh venv the
|
|
114
|
+
> suite is 482 passed, and hypswarm (118 passed) and hypmesh (37 passed) pass on it. The audit asks for a fresh
|
|
115
|
+
> independent audit before anyone claims 0.7.22 resists these attack classes. Released as 0.7.22.
|
|
116
|
+
|
|
91
117
|
## Recommendation: CONDITIONAL GO
|
|
92
118
|
|
|
93
119
|
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.22"
|
|
20
20
|
|
|
21
21
|
from .bundle import anchored, verify_bundle # noqa: E402
|
|
22
22
|
from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
|
|
@@ -154,7 +154,7 @@ def derived_numbers(bound: list[tuple[dict, dict]]) -> tuple[list[float], list[f
|
|
|
154
154
|
prim = []
|
|
155
155
|
arms = []
|
|
156
156
|
for m, rv in bound:
|
|
157
|
-
plain += _nums(m) + _nums(rv)
|
|
157
|
+
plain += _nums(m) + _nums(rv) + [f for f in (stated_figure(rv),) if f is not None]
|
|
158
158
|
a = _arm(rv)
|
|
159
159
|
if a:
|
|
160
160
|
k, n = a
|
|
@@ -199,7 +199,7 @@ def _bound(s: Snap, meas: list) -> tuple[list[tuple[dict, dict]], list[Problem]]
|
|
|
199
199
|
f"{m['result']} has no value '{m['key']}'"))
|
|
200
200
|
continue
|
|
201
201
|
bad = False
|
|
202
|
-
for k in ("value", "k", "n"):
|
|
202
|
+
for k in ("value", "k", "n", "figure"):
|
|
203
203
|
if k in m and k in rv and isinstance(m[k], (int, float)) and isinstance(rv[k], (int, float)):
|
|
204
204
|
dec = len(str(m[k]).split(".")[1]) if "." in str(m[k]) else 0
|
|
205
205
|
if round(float(rv[k]), dec) != round(float(m[k]), dec) and not (
|
|
@@ -207,6 +207,12 @@ def _bound(s: Snap, meas: list) -> tuple[list[tuple[dict, dict]], list[Problem]]
|
|
|
207
207
|
problems.append(Problem("MEASUREMENT_MISMATCH", f"measurement {m.get('name') or i + 1}: says "
|
|
208
208
|
f"{k}={m[k]} but result {m['result']} has {rv[k]}"))
|
|
209
209
|
bad = True
|
|
210
|
+
said, f = _COUNT_FIGURE.search(str(m.get("name") or "")), stated_figure(rv)
|
|
211
|
+
if not bad and said and f is not None and float(said.group(1).replace(",", "")) != f:
|
|
212
|
+
problems.append(Problem("MEASUREMENT_MISMATCH", f"measurement {m.get('name')}: its name says "
|
|
213
|
+
f"{said.group(0).strip()}, but result {m['result']} counted '{m['key']}' against "
|
|
214
|
+
f"{_fmt(f)}", fix=f"name the count after {_fmt(f)}, the figure it was made against"))
|
|
215
|
+
bad = True
|
|
210
216
|
if not bad:
|
|
211
217
|
good.append((m, rv))
|
|
212
218
|
return good, problems
|
|
@@ -253,18 +259,106 @@ def _enumerated(stmt: str, word: str) -> bool:
|
|
|
253
259
|
_PROVEN = re.compile(r"^prov(?:es?|en|ed)$|^proof$", re.I)
|
|
254
260
|
|
|
255
261
|
|
|
262
|
+
# the figure a count is about: "fitting selections worth more than 93", "routes shorter than 26 km" (or, matching a
|
|
263
|
+
# figure the claim names, "worth_more_93"). A count of nothing beyond 140 says nothing about 93 (audit HG-020-01/02).
|
|
264
|
+
_COUNT_FIGURE = re.compile(r"\b(?:(?:more|less|fewer|greater|higher|lower|better|worse|shorter|longer|cheaper|faster|"
|
|
265
|
+
r"slower|larger|smaller)\s+than|above|below|over|under|beyond|exceed\w*|beat\w*)\s+\$?"
|
|
266
|
+
r"(-?\d[\d,]*(?:\.\d+)?)", re.I)
|
|
267
|
+
_KEY_FIGURE = re.compile(r"\b(?:more|less|fewer|greater|higher|lower|better|worse|shorter|longer|cheaper|faster|"
|
|
268
|
+
r"slower|larger|smaller|above|below|over|under|beyond|exceed\w*|beat\w*)(?:\s+than)?\s+"
|
|
269
|
+
r"(\d+(?:\.\d+)?)$", re.I)
|
|
270
|
+
# the figure an absolute names in its own clause: "nothing worth more than 93 fits", "no route is shorter than 26 km"
|
|
271
|
+
_CLAIM_FIGURE = re.compile(r"\b(?:more|less|fewer|greater|higher|lower|better|worse|shorter|longer|cheaper|faster|"
|
|
272
|
+
r"slower|larger|smaller)\s+than\s+\$?(-?\d[\d,]*(?:\.\d+)?)|\b(?:above|below|beyond|"
|
|
273
|
+
r"exceed(?:s|ing)?)\s+\$?(-?\d[\d,]*(?:\.\d+)?)", re.I)
|
|
274
|
+
_SUBCLAUSE = re.compile(r",(?!\d)|[;:!?()]|\.(?!\d)|\b(?:and|but|while|whereas|although|so)\b", re.I)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def stated_figure(rv: dict) -> float | None:
|
|
278
|
+
"""The figure a count says it is about, written by the instrument beside it: {"k": 2, "n": 1024, "figure": 93} is
|
|
279
|
+
two found beyond 93."""
|
|
280
|
+
f = rv.get("figure")
|
|
281
|
+
return float(f) if isinstance(f, (int, float)) and not isinstance(f, bool) else None
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _count_figures(m: dict, rv: dict, figs: set[float] = frozenset()) -> set[float]:
|
|
285
|
+
"""Every figure a bound count could be about; empty when nothing says. A figure the result states is the only one:
|
|
286
|
+
the instrument wrote it with the count. Otherwise the name's figure ("more than 93") and the key's: a key is an
|
|
287
|
+
identifier, and the number at its end may only tell repeated counts apart ("fitting_selections_worth_more_2" is
|
|
288
|
+
the second count, not one beyond 2), so it is read only when the claim names it or the name names another. The
|
|
289
|
+
author writes the name, so a name that disagrees with the key adds a figure and never replaces one: renaming the
|
|
290
|
+
count beyond 93 "worth more than 140" no longer moves it off "the highest is 93" (audit HG-021-01)."""
|
|
291
|
+
f = stated_figure(rv)
|
|
292
|
+
if f is not None:
|
|
293
|
+
return {f}
|
|
294
|
+
out: set[float] = set()
|
|
295
|
+
hit = _COUNT_FIGURE.search(str(m.get("name") or ""))
|
|
296
|
+
if hit:
|
|
297
|
+
out.add(float(hit.group(1).replace(",", "")))
|
|
298
|
+
hit = _KEY_FIGURE.search(str(m.get("key") or "").replace("_", " "))
|
|
299
|
+
if hit and (float(hit.group(1)) in figs or out):
|
|
300
|
+
out.add(float(hit.group(1)))
|
|
301
|
+
return out
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _absolute_figures(stmt: str, w: str) -> set[float]:
|
|
305
|
+
"""The figures an absolute is about: one its own clause compares against ("nothing worth more than 93 fits"),
|
|
306
|
+
else the figures the statement's extremes name ("the highest is 93; nothing worth more fits")."""
|
|
307
|
+
figs: set[float] = set()
|
|
308
|
+
for m in re.finditer(r"\b" + re.escape(w) + r"\b", stmt, re.I):
|
|
309
|
+
end = next((x.start() for x in _SUBCLAUSE.finditer(stmt, m.end())), len(stmt))
|
|
310
|
+
figs |= {float((h.group(1) or h.group(2)).replace(",", "")) for h in _CLAIM_FIGURE.finditer(stmt[m.end():end])}
|
|
311
|
+
return figs or _extreme_figures(stmt)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _bears(m: dict, rv: dict, a: tuple[int, int], figs: set[float]) -> bool:
|
|
315
|
+
"""Does this count bear on a claim about ``figs``? One about another figure doesn't, whatever it finds: a zero
|
|
316
|
+
beyond 140 can't vouch for 93, and two beyond 93 can't refute 95. When it could be about more than one figure (a
|
|
317
|
+
name that disagrees with its key), a count that finds some bears if any of them is the claim's, and a count of
|
|
318
|
+
none or all only if every one is: a label can widen what a count refutes, never what it vouches for. A count that
|
|
319
|
+
names no figure, or a claim that names none, is read as before."""
|
|
320
|
+
c = _count_figures(m, rv, figs)
|
|
321
|
+
if not c or not figs:
|
|
322
|
+
return True
|
|
323
|
+
return bool(c & figs) if 0 < a[0] < a[1] else c <= figs
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _pinned(m: dict, rv: dict, a: tuple[int, int], figs: set[float]) -> bool:
|
|
327
|
+
"""A count at the claim's own figure that finds exceptions: it refutes the claim whatever else is bound beside it."""
|
|
328
|
+
return 0 < a[0] < a[1] and bool(figs) and bool(_count_figures(m, rv, figs) & figs)
|
|
329
|
+
|
|
330
|
+
|
|
256
331
|
def _absolutes(stmt: str, scope: str, body: dict, bound: list) -> list[Problem]:
|
|
332
|
+
"""Every absolute in the statement, and every extreme, checked on its own: a true "nothing worth more than 140
|
|
333
|
+
fits" used to end the check, and "the highest is 93" beside it went in unexamined (audit HG-021-02)."""
|
|
257
334
|
words = [w for w in L.asserted(L.ABSOLUTE, stmt) if not _enumerated(stmt, w)]
|
|
258
335
|
if any(str(m.get("key") or "").lower() in ("gap", "bound") for m, _rv in bound):
|
|
259
336
|
words = [w for w in words if not _PROVEN.match(w)] # an optimum with its bound (gap 0) is proven
|
|
260
337
|
words = [w for w in words if not _names_counted_set(stmt, w, bound)]
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
w
|
|
264
|
-
|
|
338
|
+
out: list[Problem] = []
|
|
339
|
+
seen: set[frozenset] = set()
|
|
340
|
+
for w in words: # one check per figure the absolutes are about
|
|
341
|
+
figs = frozenset(_absolute_figures(stmt, w))
|
|
342
|
+
if figs not in seen:
|
|
343
|
+
seen.add(figs)
|
|
344
|
+
out += [p for p in _absolute(stmt, scope, body, bound, w, words, set(figs)) if p.text() not in
|
|
345
|
+
{q.text() for q in out}]
|
|
346
|
+
if not any(p.code == "COUNT_CONTRADICTS" for p in out):
|
|
347
|
+
out += _superlative_contradicted(stmt, bound)
|
|
348
|
+
return out
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _absolute(stmt: str, scope: str, body: dict, bound: list, w: str, words: list, figs: set[float]) -> list[Problem]:
|
|
352
|
+
arms = [(m, rv, a) for m, rv in bound if (a := _arm(rv)) and _bears(m, rv, a, figs)]
|
|
353
|
+
about = L.content_words(stmt)
|
|
354
|
+
# a count at the claim's own figure that finds exceptions refutes it, whatever else is bound beside it
|
|
355
|
+
pinned = [(m, a) for m, rv, a in arms if _pinned(m, rv, a, figs)
|
|
356
|
+
and L.content_words(f"{m.get('name', '')} {m.get('key', '')}") & about]
|
|
357
|
+
if pinned:
|
|
358
|
+
m, (k, n) = max(pinned, key=lambda x: x[1][1])
|
|
359
|
+
return [_count_contradicts(stmt, w, words, m, k, n)]
|
|
265
360
|
exact = [(m, rv, a) for m, rv, a in arms if a[0] in (0, a[1])]
|
|
266
361
|
if not exact:
|
|
267
|
-
about = L.content_words(stmt)
|
|
268
362
|
against = [(m, a) for m, _rv, a in arms if 0 < a[0] < a[1]
|
|
269
363
|
and L.content_words(f"{m.get('name', '')} {m.get('key', '')}") & about]
|
|
270
364
|
if against:
|
|
@@ -411,9 +505,17 @@ def _superlative_contradicted(stmt: str, bound: list) -> list[Problem]:
|
|
|
411
505
|
if not sup:
|
|
412
506
|
return []
|
|
413
507
|
about = L.content_words(stmt)
|
|
414
|
-
|
|
508
|
+
figs = _extreme_figures(stmt)
|
|
509
|
+
rel = [(m, rv, a) for m, rv in bound if (a := _arm(rv)) and _bears(m, rv, a, figs)
|
|
415
510
|
and (L.content_words(f"{m.get('name', '')} {m.get('key', '')}") & about
|
|
416
|
-
or _BEYOND.search(f"{m.get('name', '')} {m.get('key', '')}".replace("_", " "))
|
|
511
|
+
or _BEYOND.search(f"{m.get('name', '')} {m.get('key', '')}".replace("_", " "))
|
|
512
|
+
or stated_figure(rv) in figs)]
|
|
513
|
+
# a count beyond the extreme's own figure that finds some refutes it, whatever else is bound beside it
|
|
514
|
+
pinned = [(m, a) for m, rv, a in rel if _pinned(m, rv, a, figs) and not _qualified(stmt, *a)]
|
|
515
|
+
rel = [(m, a) for m, _rv, a in rel]
|
|
516
|
+
if pinned:
|
|
517
|
+
m, (k, n) = max(pinned, key=lambda x: x[1][1])
|
|
518
|
+
return [_count_contradicts(stmt, sup[0], sup, m, k, n)]
|
|
417
519
|
if not rel or any(a[0] in (0, a[1]) for _m, a in rel):
|
|
418
520
|
return []
|
|
419
521
|
against = [(m, a) for m, a in rel if 0 < a[0] < a[1] and not _qualified(stmt, *a)]
|
|
@@ -487,6 +589,24 @@ def _subject_numbers(stmt: str, k: int, n: int) -> list[float]:
|
|
|
487
589
|
_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
590
|
re.I)
|
|
489
591
|
_PASSAGE_NEGATION = re.compile(r"\b(not|no|never|none|nor|neither|without)\b|n't\b", re.I)
|
|
592
|
+
# where a negation's reach ends: "every weight is in kilograms, not pounds" negates pounds, not kilograms
|
|
593
|
+
_NEGATION_ENDS = re.compile(r",(?!\d)|[;:()]|\b(?:but|while|whereas|although|though|yet|instead)\b", re.I)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _unnegated(sent: str) -> str:
|
|
597
|
+
"""A source sentence less what its negations reach: from each "not" / "never" / "no" ... to the end of its clause.
|
|
598
|
+
"Every recorded weight is in kilograms, not pounds" still asserts kilograms; "the weights were not recorded in
|
|
599
|
+
kilograms" asserts nothing about kilograms. (0.7.20 dropped any sentence with a negation in it, so a source saying
|
|
600
|
+
"kilograms, not pounds" supported a claim of pounds; audit HG-020-03.)"""
|
|
601
|
+
out, i = [], 0
|
|
602
|
+
for m in _PASSAGE_NEGATION.finditer(sent):
|
|
603
|
+
if m.start() < i:
|
|
604
|
+
continue
|
|
605
|
+
out.append(sent[i:m.start()])
|
|
606
|
+
end = _NEGATION_ENDS.search(sent, m.end())
|
|
607
|
+
i = end.start() if end else len(sent)
|
|
608
|
+
out.append(sent[i:])
|
|
609
|
+
return "".join(out)
|
|
490
610
|
|
|
491
611
|
|
|
492
612
|
def denied_words(stmt: str) -> set[str]:
|
|
@@ -548,31 +668,109 @@ def _asserted_words(sent: str) -> list[str]:
|
|
|
548
668
|
|
|
549
669
|
|
|
550
670
|
def _affirmed_in(passage: str, stem: str) -> str:
|
|
551
|
-
"""The sentence of ``passage`` that asserts a word with this stem
|
|
671
|
+
"""The sentence of ``passage`` that asserts a word with this stem outside the reach of any negation, or ''."""
|
|
552
672
|
for sent in re.split(r"(?<=[.!?;])\s+", passage or ""):
|
|
553
|
-
if stem in L.content_words(
|
|
673
|
+
if stem in L.content_words(_unnegated(sent)):
|
|
554
674
|
return sent.strip()
|
|
555
675
|
return ""
|
|
556
676
|
|
|
557
677
|
|
|
558
678
|
def _doubted_in(passage: str, doubted: set[str]) -> tuple[list[str], str] | None:
|
|
559
|
-
"""The words a
|
|
560
|
-
statement doubts, and the sentence. Doubting a source's subject
|
|
561
|
-
doubting what it says of it ("the container takes at most 36 kg")."""
|
|
679
|
+
"""The words a sentence of ``passage`` asserts outside the reach of any negation ("every weight ... is in
|
|
680
|
+
kilograms, not pounds" asserts kilograms) that the statement doubts, and the sentence. Doubting a source's subject
|
|
681
|
+
("whether the container is available") is not doubting what it says of it ("the container takes at most 36 kg")."""
|
|
562
682
|
if not doubted:
|
|
563
683
|
return None
|
|
564
684
|
for sent in re.split(r"(?<=[.!?;])\s+", passage or ""):
|
|
565
|
-
|
|
566
|
-
continue
|
|
567
|
-
hit = list(dict.fromkeys(w for w in _asserted_words(sent) if w in doubted))
|
|
685
|
+
hit = list(dict.fromkeys(w for w in _asserted_words(_unnegated(sent)) if w in doubted))
|
|
568
686
|
if hit:
|
|
569
687
|
return hit, sent.strip()
|
|
570
688
|
return None
|
|
571
689
|
|
|
572
690
|
|
|
691
|
+
# units of one kind rule each other out: weights recorded in kilograms are not recorded in pounds. Only units and
|
|
692
|
+
# currencies, where the alternatives are known; a claim that contradicts a source in meaning outside these families
|
|
693
|
+
# isn't caught here (audit HG-021-03).
|
|
694
|
+
_UNIT_FAMILIES = {
|
|
695
|
+
"mass": {"kilogram": "kilogram kilograms kilo kilos kg kgs", "gram": "gram grams", "milligram": "milligram "
|
|
696
|
+
"milligrams mg", "tonne": "tonne tonnes ton tons", "pound": "pound pounds lb lbs",
|
|
697
|
+
"ounce": "ounce ounces oz"},
|
|
698
|
+
"length": {"metre": "metre metres meter meters", "kilometre": "kilometre kilometres kilometer kilometers km kms",
|
|
699
|
+
"centimetre": "centimetre centimetres centimeter centimeters cm", "millimetre": "millimetre millimetres "
|
|
700
|
+
"millimeter millimeters mm", "mile": "mile miles", "yard": "yard yards", "foot": "foot feet ft",
|
|
701
|
+
"inch": "inch inches"},
|
|
702
|
+
"temperature": {"celsius": "celsius centigrade", "fahrenheit": "fahrenheit", "kelvin": "kelvin"},
|
|
703
|
+
"volume": {"litre": "litre litres liter liters", "millilitre": "millilitre millilitres milliliter milliliters ml",
|
|
704
|
+
"gallon": "gallon gallons", "pint": "pint pints", "quart": "quart quarts"},
|
|
705
|
+
"currency": {"dollar": "dollar dollars usd", "euro": "euro euros eur", "pound": "pound pounds gbp sterling",
|
|
706
|
+
"yen": "yen jpy", "yuan": "yuan renminbi cny", "rupee": "rupee rupees inr", "franc": "franc francs chf",
|
|
707
|
+
"peso": "peso pesos"},
|
|
708
|
+
}
|
|
709
|
+
_UNIT_OF = {}
|
|
710
|
+
for _fam, _units in _UNIT_FAMILIES.items():
|
|
711
|
+
for _canon, _words in _units.items():
|
|
712
|
+
for _w in _words.split():
|
|
713
|
+
_UNIT_OF.setdefault(_w, set()).add((_fam, _canon))
|
|
714
|
+
_UNIT_CLAUSE = re.compile(r",(?!\d)|[;:()]|\.(?!\d)|\b(?:and|but|while|whereas|although|though|because|since|so)\b",
|
|
715
|
+
re.I)
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def _plural(canon: str) -> str:
|
|
719
|
+
return {"foot": "feet", "inch": "inches", "celsius": "celsius", "fahrenheit": "fahrenheit", "kelvin": "kelvin",
|
|
720
|
+
"yen": "yen", "yuan": "yuan"}.get(canon, canon + "s")
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _units_in(text: str) -> set[tuple[str, str]]:
|
|
724
|
+
return {u for w in re.findall(r"[a-z]+", (text or "").lower()) for u in _UNIT_OF.get(w, ())}
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
_CONDITIONAL = re.compile(r"\b(?:if|whether|unless|suppos\w*|assum\w*|were they|had they)\b", re.I)
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def _unit_assertions(text: str) -> list[tuple[set[str], set[tuple[str, str]], str]]:
|
|
731
|
+
"""(subject words, units asserted of it, clause) for each clause with a copula, outside the reach of negation:
|
|
732
|
+
"the recorded weights are in pounds" says pounds of the recorded weights. A condition ("if the weights are
|
|
733
|
+
pounds") asserts nothing."""
|
|
734
|
+
out = []
|
|
735
|
+
for c in _UNIT_CLAUSE.split(_unnegated(text or "")):
|
|
736
|
+
cop = _ASSERTS.search(c)
|
|
737
|
+
if cop and not _CONDITIONAL.search(c[:cop.start()]) and (units := _units_in(c[cop.end():])):
|
|
738
|
+
out.append((L.content_words(c[:cop.start()]), units, c.strip()))
|
|
739
|
+
return out
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def asserted_units(stmt: str) -> set[str]:
|
|
743
|
+
"""The units (canonical names) a statement asserts of something: refusals.carried reads them to tell a unit
|
|
744
|
+
contradiction said again from a correction."""
|
|
745
|
+
return {canon for _subj, units, _c in _unit_assertions(stmt) for _fam, canon in units}
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _other_unit(passage: str, stmt: str) -> tuple[str, str, str] | None:
|
|
749
|
+
"""(the statement's unit, the source's, the source sentence) when a sentence of ``passage`` says the statement's
|
|
750
|
+
subject is in another unit of the same kind, and no clause of the statement about that subject says the source's
|
|
751
|
+
unit too (a clause that only converts, "about 2.2 pounds each", asserts nothing; "kilograms, not pounds" is a
|
|
752
|
+
denial, checked above)."""
|
|
753
|
+
claimed = _unit_assertions(stmt)
|
|
754
|
+
if not claimed:
|
|
755
|
+
return None
|
|
756
|
+
for sent in re.split(r"(?<=[.!?;])\s+", passage or ""):
|
|
757
|
+
clean = _unnegated(sent)
|
|
758
|
+
cop = list(_ASSERTS.finditer(clean))
|
|
759
|
+
if not cop:
|
|
760
|
+
continue
|
|
761
|
+
subj, said = L.content_words(clean[:cop[-1].start()]), _units_in(clean[cop[-1].end():])
|
|
762
|
+
about = [units for csubj, units, _c in claimed if csubj & subj]
|
|
763
|
+
also = {u for units in about for u in units}
|
|
764
|
+
for fam, canon in sorted(also):
|
|
765
|
+
theirs = {c for f, c in said if f == fam}
|
|
766
|
+
if theirs and canon not in theirs and not {(fam, c) for c in theirs} & also:
|
|
767
|
+
return canon, sorted(theirs)[0], sent.strip()
|
|
768
|
+
return None
|
|
769
|
+
|
|
770
|
+
|
|
573
771
|
def _source_contradicts(s: Snap, stmt: str, support: list[str]) -> list[Problem]:
|
|
574
772
|
denied, doubted = denied_words(stmt), doubted_words(stmt)
|
|
575
|
-
if not denied and not doubted:
|
|
773
|
+
if not denied and not doubted and not _unit_assertions(stmt):
|
|
576
774
|
return []
|
|
577
775
|
for src in support:
|
|
578
776
|
n = s.nodes.get(src)
|
|
@@ -600,6 +798,16 @@ def _source_contradicts(s: Snap, stmt: str, support: list[str]) -> list[Problem]
|
|
|
600
798
|
"says the opposite isn't support: correct the statement, or link the source as a "
|
|
601
799
|
"challenge",
|
|
602
800
|
fix="correct the statement, or link that source as a challenge", words=[stem])]
|
|
801
|
+
other = _other_unit(passage, stmt)
|
|
802
|
+
if other:
|
|
803
|
+
ours, theirs, sent = other
|
|
804
|
+
cite = str(n["body"].get("citation") or "").strip()
|
|
805
|
+
label = cite[:80] if cite else (n.get("code") or src)
|
|
806
|
+
return [Problem("SOURCE_CONTRADICTS",
|
|
807
|
+
f"the statement says {_plural(ours)}, but a source it links as support ({label}) says "
|
|
808
|
+
f"{_plural(theirs)}: \"{sent[:200]}\". One quantity isn't in two units of one kind, so that source isn't "
|
|
809
|
+
"support: correct the statement, or link the source as a challenge",
|
|
810
|
+
fix="correct the statement, or link that source as a challenge", words=[f"unit:{ours}"])]
|
|
603
811
|
return []
|
|
604
812
|
|
|
605
813
|
|
|
@@ -27,11 +27,13 @@ chain, separate from the record's events (verify() on the record is unchanged; v
|
|
|
27
27
|
says the false part and doesn't fix it either: the author moved off it. Logged once each time
|
|
28
28
|
the author moves off it. ``ref`` is the refusal it leaves behind.
|
|
29
29
|
|
|
30
|
-
conduct() is the per-author view of those: how often the record
|
|
30
|
+
conduct() is the per-author view of those: how often the record contradicted an author's claim, how often the
|
|
31
31
|
author corrected it, how often it said the same thing again — and where it stands now: whether its LAST word on
|
|
32
|
-
each claim
|
|
33
|
-
|
|
34
|
-
and
|
|
32
|
+
each contradicted claim still says it. A party that repeated itself once and then moved off the claim is counted as
|
|
33
|
+
having said it again, but not as still saying it. It keeps the two kinds apart: the record's own count (``by_count``)
|
|
34
|
+
and a source the author cited as support that says otherwise (``by_source``) — the second shows the citation doesn't
|
|
35
|
+
support the claim, not that the claim is false, since a source can be wrong. Conduct describes what an author did
|
|
36
|
+
after being shown, not why: it is evidence for a reviewer to weigh, not a verdict on anyone's intent.
|
|
35
37
|
|
|
36
38
|
report() turns it into the numbers worth watching: overclaims caught per author, how many were fixed and how many
|
|
37
39
|
abandoned, which rules fire most — "your agents tried to say 'never' 14 times this week; 11 came back honest".
|
|
@@ -132,7 +134,7 @@ def carried(problems: list[dict], statement: str) -> bool:
|
|
|
132
134
|
carried when one clause holds every figure it named (the '=…' words) again AND still asserts an extreme or an
|
|
133
135
|
absolute about them ("the highest value is 93", "nothing beats 93"): mentioning the old figure in a correction
|
|
134
136
|
("1 of 82 routes is shorter than 26 km") is not saying it again. A source contradiction is carried when the
|
|
135
|
-
statement still denies or doubts the same word."""
|
|
137
|
+
statement still denies or doubts the same word, or still says the unit the source ruled out."""
|
|
136
138
|
from . import checks
|
|
137
139
|
denied = checks.denied_words(statement or "")
|
|
138
140
|
clauses = [c for c in _CLAUSE.split(statement or "") if c.strip()]
|
|
@@ -140,6 +142,9 @@ def carried(problems: list[dict], statement: str) -> bool:
|
|
|
140
142
|
words = [str(w) for w in p.get("words") or []]
|
|
141
143
|
if p.get("code") == "SOURCE_CONTRADICTS" and set(words) & (denied | checks.doubted_words(statement or "")):
|
|
142
144
|
return True
|
|
145
|
+
units = {w[5:] for w in words if w.startswith("unit:")} # the unit a source ruled out
|
|
146
|
+
if p.get("code") == "SOURCE_CONTRADICTS" and units & checks.asserted_units(statement or ""):
|
|
147
|
+
return True
|
|
143
148
|
figs = [float(w[1:]) for w in words if w.startswith("=") and w[1:].replace(".", "", 1).isdigit()]
|
|
144
149
|
if p.get("code") != "COUNT_CONTRADICTS" or not figs:
|
|
145
150
|
continue
|
|
@@ -333,14 +338,17 @@ def rows(g: "Graph", inv: str = "", author: str = "", since: str = "", limit: in
|
|
|
333
338
|
|
|
334
339
|
|
|
335
340
|
def conduct(g: "Graph", inv: str = "", author: str = "") -> dict:
|
|
336
|
-
"""Per author: how often the record
|
|
341
|
+
"""Per author: how often the record contradicted a claim of theirs (its own count, or a source they cited, says
|
|
337
342
|
the opposite), how often they then corrected it, and how often they said the same thing again.
|
|
338
|
-
{author: {"shown_false", "corrected", "reasserted", "open", "still_saying", "moved_off",
|
|
343
|
+
{author: {"shown_false", "by_count", "by_source", "corrected", "reasserted", "open", "still_saying", "moved_off",
|
|
344
|
+
"ends_false", "sentence"}}. ``shown_false`` is every claim contradicted, ``by_count`` / ``by_source`` how many of
|
|
345
|
+
them the record's own count or a cited source contradicted (one claim can be both). A
|
|
339
346
|
re-assertion that was itself refused is not counted as a new time shown false. ``still_saying``: claims shown false
|
|
340
347
|
whose author's last word on them (its last fix, repeat or move away, in log order) says them again;
|
|
341
348
|
``moved_off``: claims it last moved away from without a fix; ``ends_false``: the author's last write was itself
|
|
342
349
|
shown false or said a contradicted claim again (a fresh false claim at the end counts, which ``still_saying``,
|
|
343
|
-
being per claim, can't see).
|
|
350
|
+
being per claim, can't see). ``shown_false`` is not a rate of lying: it adds up two kinds of evidence, and a cited
|
|
351
|
+
source can itself be wrong. Read the parts, and what the author did next, not the total."""
|
|
344
352
|
con = g._connect()
|
|
345
353
|
q, args = "SELECT id, kind, author, ref, codes, detail FROM refusal_log WHERE 1=1", []
|
|
346
354
|
for col, val in (("inv", inv), ("author", author)):
|
|
@@ -359,6 +367,7 @@ def conduct(g: "Graph", inv: str = "", author: str = "") -> dict:
|
|
|
359
367
|
repeats.add(r)
|
|
360
368
|
shown = {rid: a for rid, kind, a, _r, codes, _d in rows
|
|
361
369
|
if kind == "refused" and rid not in repeats and set(json.loads(codes or "[]")) & set(CONTRADICTIONS)}
|
|
370
|
+
shown_codes = {rid: set(json.loads(codes or "[]")) for rid, _k, _a, _r, codes, _d in rows if rid in shown}
|
|
362
371
|
root = {} # a repeat's own refusal → the refusal it repeated
|
|
363
372
|
for _id, kind, _a, ref, _c, det in rows:
|
|
364
373
|
if kind == "reasserted" and ref:
|
|
@@ -379,9 +388,11 @@ def conduct(g: "Graph", inv: str = "", author: str = "") -> dict:
|
|
|
379
388
|
last[r] = kind
|
|
380
389
|
out: dict[str, dict] = {}
|
|
381
390
|
for rid, a in shown.items():
|
|
382
|
-
o = out.setdefault(a, {"shown_false": 0, "
|
|
383
|
-
"moved_off": 0})
|
|
391
|
+
o = out.setdefault(a, {"shown_false": 0, "by_count": 0, "by_source": 0, "corrected": 0, "reasserted": 0,
|
|
392
|
+
"open": 0, "still_saying": 0, "moved_off": 0})
|
|
384
393
|
o["shown_false"] += 1
|
|
394
|
+
o["by_count"] += "COUNT_CONTRADICTS" in shown_codes[rid]
|
|
395
|
+
o["by_source"] += "SOURCE_CONTRADICTS" in shown_codes[rid]
|
|
385
396
|
fixed = any(k == "fixed" and r == rid for _i, k, _a, r, _c, _d in rows)
|
|
386
397
|
again = sum(1 for _i, k, _a, r, _c, _d in rows if k == "reasserted" and r == rid)
|
|
387
398
|
o["corrected"] += fixed
|
|
@@ -409,8 +420,12 @@ def conduct(g: "Graph", inv: str = "", author: str = "") -> dict:
|
|
|
409
420
|
o["ends_false"] = any(k == "reasserted" or (k == "refused" and set(json.loads(c or "[]")) & set(CONTRADICTIONS))
|
|
410
421
|
for k, c in rs) and not _accepted_after(con, a, inv, key)
|
|
411
422
|
for a, o in out.items():
|
|
412
|
-
|
|
413
|
-
|
|
423
|
+
c, s_ = o["by_count"], o["by_source"]
|
|
424
|
+
shown = "; ".join(
|
|
425
|
+
([f"the record's own count contradicted a claim of this author's {c} time{'s' * (c != 1)}"] if c else [])
|
|
426
|
+
+ ([f"{s_} claim{'s' * (s_ != 1)} of this author's went against a source it cited as support"]
|
|
427
|
+
if s_ else []))
|
|
428
|
+
o["sentence"] = (shown + "; "
|
|
414
429
|
+ (f"the author said it again {o['reasserted']} time{'s' * (o['reasserted'] != 1)} after "
|
|
415
430
|
"being shown" if o["reasserted"] else "the author did not say it again")
|
|
416
431
|
# "never corrected" only beside a repeat: dropping a claim for a different one is no repeat
|
|
@@ -418,7 +433,7 @@ def conduct(g: "Graph", inv: str = "", author: str = "") -> dict:
|
|
|
418
433
|
if o["corrected"] else ", and never corrected it"
|
|
419
434
|
if o["reasserted"] and (o["still_saying"] or o["ends_false"]) else "")
|
|
420
435
|
+ ("; its last word still says it" if o["still_saying"] else
|
|
421
|
-
"; its last word was
|
|
436
|
+
"; its last word was contradicted by the record" if o["ends_false"] else
|
|
422
437
|
"; its last word no longer says it" if o["reasserted"] else ""))
|
|
423
438
|
return out
|
|
424
439
|
|