hypgraph 0.7.28__tar.gz → 0.8.0__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.28 → hypgraph-0.8.0}/CHANGELOG.md +38 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/PKG-INFO +1 -1
- {hypgraph-0.7.28 → hypgraph-0.8.0}/docs/PLAN.md +1 -1
- {hypgraph-0.7.28 → hypgraph-0.8.0}/docs/RELEASE-READINESS.md +21 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/docs/spec.md +52 -3
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/__init__.py +1 -1
- hypgraph-0.8.0/src/hypgraph/assertions.py +384 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/bundle.py +15 -3
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/checks.py +142 -9
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/derive.py +22 -1
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/graph.py +11 -2
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/mcp.py +7 -2
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/plan.py +1 -1
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/profiles.py +1 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/refusals.py +20 -13
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/schema.py +2 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/skill/SKILL.md +11 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/stats.py +8 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/ui/hypgraph.i18n.js +12 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/ui/hypgraph.js +12 -2
- hypgraph-0.8.0/tests/test_assertions.py +390 -0
- hypgraph-0.8.0/tests/test_typed_audits.py +166 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/.gitignore +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/AGENTS.md +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/CITATION.cff +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/DISCLAIMER.md +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/LICENSE +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/NOTICE +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/README.md +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/docs/decisions/ADR-002-governance-profiles.md +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/docs/profile-workflows-plan.md +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/docs/review-2026-10-24.md +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/docs/site-plan.md +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/docs/ui-plan.md +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/docs/verification.md +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/docs/workflows/README.md +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/docs/workflows/knowledge.md +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/docs/workflows/optimization.md +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/docs/workflows/scientific.md +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/docs/workflows/software.md +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/docs/workflows/trading.md +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/examples/frameworks/README.md +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/pyproject.toml +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/__main__.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/blind.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/cli.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/coord.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/demos/__init__.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/demos/broad_street.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/demos/linked.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/language.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/policy.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/references.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/report_check.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/server.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/store.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/ui/__init__.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/ui/atlas.css +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/ui/atlas.js +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/ui/hypgraph.css +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/ui/hypgraph.svg +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/ui/index.html +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/ui/vendor/cose-base.js +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/ui/vendor/layout-base.js +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/src/hypgraph/workflow.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_accept_work_kind.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_anchoring.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_blind.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_blind_oracles.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_borrowed_words.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_bound_support.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_bundle_telemetry.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_cancel_releases_lease.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_cited_and_failed_support.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_contradictions.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_coord.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_counts_that_bear.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_d10.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_direction_and_units.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_engine.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_engine_fixes.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_figure_said_twice.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_framework_examples.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_i18n.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_incremental.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_link_translate.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_linked_records.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_log_and_check.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_measure_questions.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_opposed_answers.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_option_lead.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_order_detail.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_profiles.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_profiles_041.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_quantifiers_and_identity.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_questions_not_assertions.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_references.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_refusal_friction.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_refusal_results.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_refusals.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_relation_words.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_report_answers.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_rerunnable.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_restates_scale.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_server_auth.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_server_hosts.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_shape_refusals.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_stats_not_backtests.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_stats_values.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_surfaces.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_telemetry_stream.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_trust_boundary.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_ui_app.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_ui_engine.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_uncorrected_effect.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_unnamed_inputs.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_verify_complete.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_verify_lifecycle.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_verify_scope.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_verify_scoped_views.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_verify_trust.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_verify_views.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_versions_not_figures.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_vocabulary_friction.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_what_a_count_is_of.py +0 -0
- {hypgraph-0.7.28 → hypgraph-0.8.0}/tests/test_workflows.py +0 -0
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0 — 2026-11-01: typed claim assertions
|
|
4
|
+
|
|
5
|
+
Eight independent audits (HG-020 → HG-028) found the same class of failure, each time in a new wording: to decide
|
|
6
|
+
whether a count backs or contradicts a claim, the checks read words — the result's key and the claimant's
|
|
7
|
+
measurement name for what was counted, the statement for what is asserted and which clause a count belongs to — and
|
|
8
|
+
the claimant writes most of those words. Each fix narrowed one wording; the next audit found another. 0.8.0 stops
|
|
9
|
+
inferring that structure from words.
|
|
10
|
+
|
|
11
|
+
- **Typed counts.** A count may say what it counted: `population` (a path, `route/passenger` lies within `route`),
|
|
12
|
+
`property` and `unit`, beside `k`, `n`, `figure` and `op`. The result's author writes them; the instrument, on its
|
|
13
|
+
own results.
|
|
14
|
+
- **Typed assertions.** A claim may carry `asserts`, atoms `{quantifier none | all | some | exactly, population,
|
|
15
|
+
property, op, figure, unit, n, k, measurement}`. Each is decided against its bound count by sets on the number line
|
|
16
|
+
and population paths (`src/hypgraph/assertions.py`): supported, contradicted, or unresolved with the field that
|
|
17
|
+
blocked it. No words are read, so a true atom can't lend anything to a false one (HG-028-01) and "all 4 routes"
|
|
18
|
+
meets its counterexample however the set is enumerated (HG-028-02). New codes `ASSERTION_CONTRADICTED` (tracked in
|
|
19
|
+
conduct like `COUNT_CONTRADICTS`, `by_assertion`) and `ASSERTION_UNRESOLVED`.
|
|
20
|
+
- **Typed investigations.** `typed_claims: true` on an investigation (or a profile's `typed_claims`) makes atoms the
|
|
21
|
+
check: a statement that asserts an absolute, an extreme or a count needs an atom for each part that does
|
|
22
|
+
(`ASSERTION_MISSING`, with atoms drafted from the statement and its typed counts — never trusted, checked once
|
|
23
|
+
written), and the word interpretation stands down there: measurement names and keys aren't read for what was
|
|
24
|
+
counted, and the absolute / counterexample / extreme checks don't run. Numbers in the statement must still be bound;
|
|
25
|
+
every other check runs as before.
|
|
26
|
+
- **Standing.** A claim's state carries `assertions` (each atom rendered as the sentence it asserts, with its verdict
|
|
27
|
+
against the count as it stands now), `assertion_contradicted` when a count revised later makes an atom false (the
|
|
28
|
+
claim joins the revalidation queue), and `checked_by`: `typed`, `wording` or `none`. Bundles carry the verdicts, and
|
|
29
|
+
the verifier checks them against the rows. The Atlas claim drawer shows the atoms as the headline and the author's
|
|
30
|
+
sentence beneath, labelled as prose.
|
|
31
|
+
- **A count is whole, for everyone** (HG-028-03). `k` and `n` must be whole numbers (an int or an integral float),
|
|
32
|
+
finite, `0 ≤ k ≤ n`, `n ≥ 1`, on creation and on revision (`VALUE_NOT_A_COUNT`); counts used to be truncated, so 0.9
|
|
33
|
+
of 4 read as 0 of 4 and backed "none". A typed count must be complete (`VALUE_TYPE_INVALID`).
|
|
34
|
+
- **The word checks are frozen.** Outside a typed investigation they run as in 0.7.28 and are documented as heuristics;
|
|
35
|
+
HG-028-01 and -02 remain limits of untyped mode. Replaying the 1,900 recorded adversarial entries changes none, and
|
|
36
|
+
every trial's standing is the same.
|
|
37
|
+
- Tests: `tests/test_assertions.py` (the interval logic checked against enumeration over every op and quantifier, paths,
|
|
38
|
+
units, the gate, standing, bundles, the draft) and `tests/test_typed_audits.py` (92 attack sentences from the audits
|
|
39
|
+
and their holdouts, around fixed atoms: none changes a verdict).
|
|
40
|
+
|
|
3
41
|
## 0.7.28 — 2026-11-01: a figure said twice, a comparison in another tense
|
|
4
42
|
|
|
5
43
|
Fixes from an independent audit of 0.7.27 (HG-027-01, -02) and from paraphrases written after the first fix; each case
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hypgraph
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
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.
|
|
112
|
+
## Status at 0.8.0 (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
|
|
@@ -213,6 +213,27 @@ was taken from a changelog without being checked against the code.*
|
|
|
213
213
|
> checked against a counterexample. This is still word matching; the typed claim layer the audits recommend is not
|
|
214
214
|
> built. Released as 0.7.28.
|
|
215
215
|
|
|
216
|
+
> **Audit of 0.7.28 (2026-11-01): the class, not the symptom.** A ninth independent audit named three defects, all
|
|
217
|
+
> reproduced on the PyPI 0.7.28 wheel (9 of 12 reproducer cases failed, the 3 controls passed): a true absolute about
|
|
218
|
+
> one thing still lent its words to a false one about another in the same statement (HG-028-01); an enumerated "all 4
|
|
219
|
+
> routes" skipped the counterexample check (HG-028-02); and fractional counts were truncated, so 0.9 of 4 read as 0 of
|
|
220
|
+
> 4 (HG-028-03). The first two are the class every audit since HG-020 found: the checks infer from the claimant's
|
|
221
|
+
> words what a count measured, what a claim asserts and which clause a count belongs to. That is now addressed
|
|
222
|
+
> structurally rather than per wording: counts carry types (population, property, unit), claims carry typed atoms, and
|
|
223
|
+
> an atom is decided against its count by interval logic and population paths, reading no words
|
|
224
|
+
> (`src/hypgraph/assertions.py`). The audit's typed-predicate recommendation is the design. Fractional and impossible
|
|
225
|
+
> counts are refused for everyone (`VALUE_NOT_A_COUNT`).
|
|
226
|
+
>
|
|
227
|
+
> **Where that stands.** Typed checking is opt-in per investigation (`typed_claims`), additive to `hypgraph/1`.
|
|
228
|
+
> Inside it, the audits' 92 attack sentences around fixed atoms change no verdict, and the interval logic is checked
|
|
229
|
+
> against enumeration for every op and quantifier. Outside it the word checks are frozen at 0.7.28 and labelled
|
|
230
|
+
> heuristic: HG-028-01 and -02 are known limits of untyped mode, closed only by typed mode. Typed checking covers
|
|
231
|
+
> count-based assertions (none / all / some / exactly, and extremes as pairs); comparisons between two measured arms
|
|
232
|
+
> and significance are still word-checked. In typed mode the prose remains the author's: a sentence that asserts
|
|
233
|
+
> more than its atoms is refused (`ASSERTION_MISSING`), but a detection miss leaves prose unverified rather than
|
|
234
|
+
> verified, and the record shows the atoms, not the sentence, as what it checked. Replaying the 1,900 recorded
|
|
235
|
+
> adversarial entries changes none. #102 is merged with green CI (17 of 17 checks). Released as 0.8.0.
|
|
236
|
+
|
|
216
237
|
## Recommendation: CONDITIONAL GO
|
|
217
238
|
|
|
218
239
|
HypGraph is ready for a public developer preview once #59, #60 and #61 are merged with green CI and released as one
|
|
@@ -20,10 +20,10 @@ correction is a new row, never an edited one.
|
|
|
20
20
|
|
|
21
21
|
| type | code | fields (\* required) | what it is |
|
|
22
22
|
|---|---|---|---|
|
|
23
|
-
| `investigation` | INV | **title**\*, **objective**\*, success_criteria, scope, state, reviewers, constants, strict_claims, keywords, projects, doi, note, legacy_case | The study: an objective, its scope, who reviews it. Every other node belongs to one. |
|
|
23
|
+
| `investigation` | INV | **title**\*, **objective**\*, success_criteria, scope, state, reviewers, constants, strict_claims, typed_claims, keywords, projects, doi, note, legacy_case | The study: an objective, its scope, who reviews it. Every other node belongs to one. |
|
|
24
24
|
| `question` | Q | **text**\*, why | Something you want to know. |
|
|
25
25
|
| `hypothesis` | H | **statement**\*, assumptions, **predictions**\*, distinguishing, open_questions | A possible answer, with the predictions that would show it wrong. |
|
|
26
|
-
| `claim` | CL | **statement**\*, scope, limitations, kind, measurements, evidence_note, exposure, exposure_unit, legacy | What you assert is known, with its scope and its numbers bound to results. |
|
|
26
|
+
| `claim` | CL | **statement**\*, scope, limitations, kind, measurements, asserts, evidence_note, exposure, exposure_unit, legacy | What you assert is known, with its scope and its numbers bound to results. |
|
|
27
27
|
| `experiment` | X | **aim**\*, **protocol**\*, controls, **success_criteria**\*, method, budget, power_check, cost_minutes, seed | A planned test: aim, protocol, controls, success criteria, and what each hypothesis expects. |
|
|
28
28
|
| `run` | RUN | **kind**\*, job_id, name, state, code, inputs, outputs, args, config, seed, env, started, finished, exit_code, log_sha256, note | One execution: code, inputs and outputs hashed, seed, state. `kind=lab` only from an instrument. |
|
|
29
29
|
| `result` | RS | **summary**\*, kind, outcome, values, expected, limitations, power, evidence_note, legacy | What a run (or an observation, or a citation) found: values, outcome, power. |
|
|
@@ -59,6 +59,21 @@ correction is a new row, never an edited one.
|
|
|
59
59
|
Field kinds and lengths: `hypgraph schema --json` (or `/schema/hypgraph-1.json` on the site). A claim's
|
|
60
60
|
`measurements` are `{name, value | k & n | lo & hi, result: <result id>, key: <field of that result's values>}`.
|
|
61
61
|
|
|
62
|
+
**Counts and typed counts.** A result value with `k` or `n` is a count: `k` of `n`, both whole numbers (an int, or a
|
|
63
|
+
float with no fraction), finite, `0 ≤ k ≤ n`, `n ≥ 1`. It may say what it compared: `figure` and `op` (one of `<` `<=`
|
|
64
|
+
`=` `!=` `>=` `>`). A **typed count** also says what it counted: `population`, a path such as `route/passenger` (lower
|
|
65
|
+
case; `a/b` lies within `a`), `property`, an id (`length`), and optionally `unit`. `{"k": 1, "n": 4, "figure": 26,
|
|
66
|
+
"op": ">", "population": "route", "property": "length", "unit": "km"}` is 1 of 4 routes with length > 26 km. The
|
|
67
|
+
types are the result's author's; on an instrument-filed result they are the instrument's.
|
|
68
|
+
|
|
69
|
+
**Typed assertions (`asserts`).** A claim may state what it asserts as atoms, each checked against a typed count
|
|
70
|
+
(§7): `{quantifier, population, property, op, figure, unit?, n?, k?, measurement}`, where `quantifier` is `none`
|
|
71
|
+
(nothing in the population meets `property op figure`), `all` (everything does), `some` (at least one does) or
|
|
72
|
+
`exactly` (`k` of them do; `k` required), `n` is the population's size as the claim states it, and `measurement` is the
|
|
73
|
+
0-based index of the claim's measurement whose count decides it. An extreme is two atoms: "the longest route is 26 km"
|
|
74
|
+
is `none` `>` 26 and `some` `>=` 26. An investigation with `typed_claims: true` (or a profile that sets it) checks its
|
|
75
|
+
claims this way.
|
|
76
|
+
|
|
62
77
|
Verdicts: positive `checked`, `pass`, `reproduced`, `replicated`, `separate_implementation`, `generalizes`; negative
|
|
63
78
|
`fail`, `not_reproduced`, `not_replicated`, `does_not_generalize`; and `objection`, `resolved`, `inconclusive`,
|
|
64
79
|
`comment`.
|
|
@@ -169,7 +184,8 @@ published and signed, and what each check does and doesn't show: [verification.m
|
|
|
169
184
|
verifier must check every fact in it that the rows fix: which investigations it covers (the scope's, or every
|
|
170
185
|
investigation node), which investigations the list shows (each once), which nodes each investigation's graph shows
|
|
171
186
|
(all of them, unless capped at 5,000) and which live (unretracted) links among them, each node's `type`, `code`, `inv`,
|
|
172
|
-
`by`, `at`, `v`, `title`, `kind` and `state.lifecycle`,
|
|
187
|
+
`by`, `at`, `v`, `title`, `kind` and `state.lifecycle`, each typed assertion's verdict (`state.assertions`, a function
|
|
188
|
+
of the claim's and its results' latest rows), and each link's `id`, ends, versions, `rel`, `data`, `at`,
|
|
173
189
|
`by` and `why`. Every one of these must be **present**, not only equal where present: a viewer fills a missing fact
|
|
174
190
|
with a default, and a withdrawn claim without its lifecycle would read as active. The lifecycle is derived from the
|
|
175
191
|
verified rows (withdrawn when a withdrawal exists; superseded when a live `supersedes` or `corrects` replacement is
|
|
@@ -212,6 +228,11 @@ Implementations must derive these, never store them:
|
|
|
212
228
|
- A replication edge on the run raises the profile according to its `differs`.
|
|
213
229
|
- A result of `kind=cited` is external only if it `cites` a source node.
|
|
214
230
|
- **basis_changed** (decisions): a `based_on` edge whose target's version has moved past the pinned `dst_v`.
|
|
231
|
+
- **assertions** (claims with `asserts`): each atom with its verdict against its count as the record holds it now
|
|
232
|
+
(§7, *Typed claims*) and its rendered sentence ("no route/passenger (of 4) has length > 26 km"). The rendered atoms
|
|
233
|
+
are what the record verified; the statement is the author's prose. **assertion_contradicted**: the atoms a count
|
|
234
|
+
revised since the claim was written now makes false. **checked_by**: `typed` when the claim's investigation checks
|
|
235
|
+
typed atoms, `wording` when the word checks applied, `none` with `strict_claims: false`.
|
|
215
236
|
- **written blind** (results): a work order with `blind: true` opens a window at its creation event and closes it at
|
|
216
237
|
the first release (return or cancel) or withdrawal of any order in its rework chain. A result its assignee
|
|
217
238
|
created inside the window, and has not revised since it closed, is written blind under that order, provided the
|
|
@@ -227,6 +248,9 @@ Implementations must derive these, never store them:
|
|
|
227
248
|
- Only the instrument files a `kind=lab` run, and only the instrument records what that run `produced`.
|
|
228
249
|
- Claims must pass the checks in §7, unless written by the operator or importer, or in an investigation with
|
|
229
250
|
`strict_claims: false`.
|
|
251
|
+
- A result's counts must be counts (`VALUE_NOT_A_NUMBER`, `VALUE_NOT_A_COUNT`), and a typed count must be complete
|
|
252
|
+
(`VALUE_TYPE_INVALID`: `population` a path, `property` an id, `op` one of the six, `figure` a number) — on creation
|
|
253
|
+
and on revision.
|
|
230
254
|
- Nobody officially reviews their own node.
|
|
231
255
|
- Writes into an `archived` or `published` investigation are refused (reserved authors excepted).
|
|
232
256
|
- Only the importer may set a write's time. Everyone else's comes from the writer's clock.
|
|
@@ -264,6 +288,31 @@ smallest detectable difference uses 80% power at two-sided α = 0.05.
|
|
|
264
288
|
| 10 | `EXCLUSION_WITHOUT_POWER` | `kind: exclusion`, or wording that says an effect is absent (no effect, no difference, makes no difference, does not affect, rules out, ineffective, …) in a finding, without a supporting result with `power: demonstrated` or one produced by a run of an experiment with a `power_check`. With two counts, the message gives the smallest detectable difference. |
|
|
265
289
|
| 11 | `UNCORRECTED_EFFECT` | a finding asserts an effect (changes, lowers, differs, affects, significant, …) on an outcome, while a live result in the investigation holds that outcome's corrected p-value (a field named with holm / bonferroni / fdr / adjusted / corrected …) at or above 0.05, or a count of 0 outcomes surviving correction (`n_holm_sig`), and the effect word is neither qualified within 20 words as uncorrected (uncorrected, unadjusted, nominally, without correction, …) nor names the correction with a corrected value bound as a measurement. A family-wide value (`min_adjusted_p`) applies when the claim binds a nominal p < 0.05. Thresholds (`bonferroni_alpha_threshold`) are not evidence. Honest forms: bind the corrected p and say "not significant after correction", or file `kind: lead`. |
|
|
266
290
|
|
|
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
|
+
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 (spellings of one unit are one unit; nothing is
|
|
294
|
+
converted); a stated `n` must equal the count's.
|
|
295
|
+
|
|
296
|
+
- `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
|
+
**contradicted** when a side with more than 0 lies inside S_a and P_e lies within P_a.
|
|
298
|
+
- `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
|
+
0 contains S_a and P_a lies within P_e.
|
|
300
|
+
- `all` S_a is `none` of the complement of S_a. `exactly` k needs the same population and the same set (or its
|
|
301
|
+
complement, counted n − k), then compares k.
|
|
302
|
+
- Anything else is **unresolved**, and names the field that blocked it.
|
|
303
|
+
|
|
304
|
+
| Code | Refused when |
|
|
305
|
+
|---|---|
|
|
306
|
+
| `ASSERTION_CONTRADICTED` | an atom is false by its count. The refusal log tracks it like a count contradiction (§8). |
|
|
307
|
+
| `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 has more parts that assert an absolute, an extreme or a count than the claim has atoms. 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
|
+
|
|
310
|
+
Atoms are checked in every investigation that has them. In a `typed_claims` investigation the word interpretation
|
|
311
|
+
stands down: a measurement's name and key are not read for what was counted, and rule 5 (absolutes, with the
|
|
312
|
+
counterexample, extreme and unreached-figure checks behind it) does not run. Every other rule runs as before. Outside
|
|
313
|
+
one, rule 5 and those checks are heuristics, frozen as of 0.7.28: they read a claim's words for what a count measured,
|
|
314
|
+
what the claim asserts and which clause a count belongs to, and the claimant writes those words.
|
|
315
|
+
|
|
267
316
|
**Exposure.** A claim may set `exposure` (how many uses, users or units it will be applied to) and `exposure_unit`.
|
|
268
317
|
Absolute refusals then state how many failures the bound leaves unruled out at that scale. An accepted claim's
|
|
269
318
|
standing carries `bounds` (per count: k, n, rate, lo, hi and, with exposure, `unruled_out`), its sentence reads
|
|
@@ -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.
|
|
19
|
+
__version__ = "0.8.0"
|
|
20
20
|
|
|
21
21
|
from .bundle import anchored, verify_bundle # noqa: E402
|
|
22
22
|
from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Typed claim assertions: what a claim asserts about a count, stated as data and checked against the count's own typed
|
|
3
|
+
fields. Nothing here reads words.
|
|
4
|
+
|
|
5
|
+
The word checks (checks.py) work out from prose what a count measured, what a claim asserts and which clause a count
|
|
6
|
+
belongs to. The claimant writes most of those words, and eight audits (HG-020 → HG-028) each found a new wording that
|
|
7
|
+
steered one of those inferences. Here the instrument types what it counted and the claimant types what it asserts:
|
|
8
|
+
|
|
9
|
+
a count {"k": 1, "n": 4, "figure": 26, "op": ">", "population": "route/passenger", "property": "length",
|
|
10
|
+
"unit": "km"} — 1 of the 4 passenger routes has length > 26 km
|
|
11
|
+
an atom {"quantifier": "none", "population": "route/passenger", "property": "length", "op": ">",
|
|
12
|
+
"figure": 26, "unit": "km", "n": 4, "measurement": 0} — no passenger route is longer than 26 km,
|
|
13
|
+
checked against the claim's first measurement
|
|
14
|
+
|
|
15
|
+
quantifier none (nothing in the population meets ``property op figure``) · all (everything does) · some (at least
|
|
16
|
+
one does) · exactly (``k`` of them do). An extreme is two atoms: "the highest is 93" is none > 93 and
|
|
17
|
+
some >= 93.
|
|
18
|
+
population a path: ``route/passenger`` lies within ``route``, so a count of 0 over every route also settles
|
|
19
|
+
"no passenger route"; a count of the passenger routes says nothing of the cargo routes.
|
|
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"); never converted.
|
|
22
|
+
n optional: the population's size as the claim states it; must be the count's n.
|
|
23
|
+
|
|
24
|
+
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
|
|
25
|
+
S_a = {x : x op_a figure_a} (what is asserted) — and the paths. A count of k in S_e is also a count of n - k outside it.
|
|
26
|
+
|
|
27
|
+
none S_a supported when some count of 0 is over a set containing S_a and a population containing the atom's;
|
|
28
|
+
contradicted when a count above 0 is over a set inside S_a and a population inside the atom's
|
|
29
|
+
some S_a the mirror: a count above 0 inside S_a supports it, a count of 0 containing S_a contradicts it
|
|
30
|
+
all S_a none of the complement of S_a
|
|
31
|
+
exactly the same set and the same population, then k compared
|
|
32
|
+
|
|
33
|
+
Anything else is unresolved, and the verdict names the field that blocked it. One count can back two atoms only if
|
|
34
|
+
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"
|
|
35
|
+
is checked against its counterexample whatever the sentence enumerates (HG-028-02).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import json
|
|
41
|
+
import re
|
|
42
|
+
|
|
43
|
+
from . import stats
|
|
44
|
+
from .derive import Snap, result_value
|
|
45
|
+
|
|
46
|
+
OPS = ("<", "<=", "=", "!=", ">=", ">")
|
|
47
|
+
COMPLEMENT = {"<": ">=", "<=": ">", "=": "!=", "!=": "=", ">=": "<", ">": "<="}
|
|
48
|
+
QUANTIFIERS = ("none", "all", "some", "exactly")
|
|
49
|
+
FIELDS = ("quantifier", "population", "property", "op", "figure", "unit", "n", "k", "measurement")
|
|
50
|
+
SHOW_OP = {"<": "<", "<=": "≤", "=": "=", "!=": "≠", ">=": "≥", ">": ">"}
|
|
51
|
+
_SEGMENT = re.compile(r"^[a-z0-9][a-z0-9 _.-]*$")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _num(x) -> bool:
|
|
55
|
+
return isinstance(x, (int, float)) and not isinstance(x, bool) and x == x and abs(x) != float("inf")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def path(p) -> tuple[str, ...] | None:
|
|
59
|
+
"""A population path as its segments (lower-cased, trimmed), or None when it isn't one."""
|
|
60
|
+
if not isinstance(p, str) or not p.strip():
|
|
61
|
+
return None
|
|
62
|
+
segs = tuple(" ".join(x.strip().lower().split()) for x in p.strip().strip("/").split("/"))
|
|
63
|
+
return segs if all(_SEGMENT.match(x) for x in segs) else None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def within(inner: tuple, outer: tuple) -> bool:
|
|
67
|
+
"""Does every member of ``inner`` belong to ``outer``? route/passenger lies within route."""
|
|
68
|
+
return inner[:len(outer)] == outer
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def prop(p) -> str | None:
|
|
72
|
+
"""A property id, compared for equality: case, spaces, hyphens and underscores don't matter."""
|
|
73
|
+
if not isinstance(p, str) or not p.strip():
|
|
74
|
+
return None
|
|
75
|
+
return re.sub(r"[\s_-]+", "_", p.strip().lower())
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _unit(u) -> frozenset | None:
|
|
79
|
+
"""A unit as the set of names it could be ("km" → {kilometre}); None when there's none."""
|
|
80
|
+
if u is None or (isinstance(u, str) and not u.strip()):
|
|
81
|
+
return None
|
|
82
|
+
from .checks import _UNIT_OF # one table of unit spellings, shared with the word checks
|
|
83
|
+
w = str(u).strip().lower()
|
|
84
|
+
return frozenset(c for _f, c in _UNIT_OF.get(w, ())) or frozenset({w})
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def same_unit(a, b) -> bool:
|
|
88
|
+
ua, ub = _unit(a), _unit(b)
|
|
89
|
+
return (ua is None and ub is None) or (ua is not None and ub is not None and bool(ua & ub))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def holds(x: float, op: str, f: float) -> bool:
|
|
93
|
+
return {"<": x < f, "<=": x <= f, "=": x == f, "!=": x != f, ">=": x >= f, ">": x > f}[op]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
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 intervals with ends at the two figures,
|
|
98
|
+
so membership is constant on each piece between them: checking one point per piece decides it."""
|
|
99
|
+
pts = sorted({float(a[1]), float(b[1])})
|
|
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))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def same_set(a: tuple, b: tuple) -> bool:
|
|
105
|
+
return subset(a, b) and subset(b, a)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def count_fields(rv) -> tuple[dict | None, str]:
|
|
109
|
+
"""A result value as a typed count {k, n, op, figure, population, property, unit}, or (None, what's missing)."""
|
|
110
|
+
if not isinstance(rv, dict):
|
|
111
|
+
return None, "the bound value is not a count"
|
|
112
|
+
k, n = rv.get("k"), rv.get("n")
|
|
113
|
+
if not (stats.whole(k) and stats.whole(n) and n >= 1 and 0 <= k <= n):
|
|
114
|
+
return None, "the bound value is not a count (whole k of n)"
|
|
115
|
+
for f in ("population", "property"):
|
|
116
|
+
if not rv.get(f):
|
|
117
|
+
return None, f"the count has no {f}: the result's author types what was counted"
|
|
118
|
+
if rv.get("op") not in OPS:
|
|
119
|
+
return None, "the count has no op (one of < <= = != >= >)"
|
|
120
|
+
if not _num(rv.get("figure")):
|
|
121
|
+
return None, "the count has no figure"
|
|
122
|
+
pop = path(rv["population"])
|
|
123
|
+
if pop is None:
|
|
124
|
+
return None, "the count's population is not a path (route/passenger)"
|
|
125
|
+
return {"k": int(k), "n": int(n), "op": rv["op"], "figure": float(rv["figure"]), "population": pop,
|
|
126
|
+
"property": prop(rv["property"]), "unit": rv.get("unit")}, ""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def atom_shape(a, meas: list | None) -> tuple[str, str]:
|
|
130
|
+
"""(field, why) when an atom is malformed; ("", "") when it is well formed. ``meas`` None: the measurement index
|
|
131
|
+
isn't checked."""
|
|
132
|
+
if not isinstance(a, dict):
|
|
133
|
+
return "atom", "an assertion is an object {quantifier, population, property, op, figure, measurement}"
|
|
134
|
+
extra = sorted(set(a) - set(FIELDS))
|
|
135
|
+
if extra:
|
|
136
|
+
return extra[0], f"unknown field {extra[0]!r} (fields: {', '.join(FIELDS)})"
|
|
137
|
+
if a.get("quantifier") not in QUANTIFIERS:
|
|
138
|
+
return "quantifier", "quantifier must be one of none, all, some, exactly"
|
|
139
|
+
if path(a.get("population")) is None:
|
|
140
|
+
return "population", "population must be a path such as route or route/passenger"
|
|
141
|
+
if prop(a.get("property")) is None:
|
|
142
|
+
return "property", "property must name what is compared (length, speed, wait)"
|
|
143
|
+
if a.get("op") not in OPS:
|
|
144
|
+
return "op", "op must be one of < <= = != >= >"
|
|
145
|
+
if not _num(a.get("figure")):
|
|
146
|
+
return "figure", "figure must be a number"
|
|
147
|
+
if "unit" in a and a["unit"] is not None and not isinstance(a["unit"], str):
|
|
148
|
+
return "unit", "unit must be text"
|
|
149
|
+
if "n" in a and not (stats.whole(a["n"]) and a["n"] >= 1):
|
|
150
|
+
return "n", "n must be a whole number of at least 1"
|
|
151
|
+
if a["quantifier"] == "exactly" and not (stats.whole(a.get("k")) and a["k"] >= 0):
|
|
152
|
+
return "k", "'exactly' needs k, a whole number"
|
|
153
|
+
if "k" in a and a["quantifier"] != "exactly":
|
|
154
|
+
return "k", "k is for 'exactly'; none / all / some say it themselves"
|
|
155
|
+
i = a.get("measurement")
|
|
156
|
+
if meas is None:
|
|
157
|
+
return "", ""
|
|
158
|
+
if not (stats.whole(i) and 0 <= i < len(meas) and isinstance(meas[int(i)], dict)):
|
|
159
|
+
return "measurement", f"measurement must index the claim's measurements (0..{len(meas) - 1})" if meas else \
|
|
160
|
+
"measurement must index the claim's measurements, and the claim binds none"
|
|
161
|
+
return "", ""
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _pop(p: tuple) -> str:
|
|
165
|
+
return "/".join(p)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _cond(a: dict) -> str:
|
|
169
|
+
u = f" {a['unit']}" if a.get("unit") else ""
|
|
170
|
+
fig = stats_fmt(a["figure"])
|
|
171
|
+
return f"{a['property']} {SHOW_OP.get(a['op'], a['op'])} {fig}{u}"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def stats_fmt(v) -> str:
|
|
175
|
+
v = float(v)
|
|
176
|
+
return str(int(v)) if v.is_integer() else f"{v:g}"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def render(a: dict) -> str:
|
|
180
|
+
"""An atom as the sentence it asserts — what the record shows as verified."""
|
|
181
|
+
if atom_shape(a, None)[0]:
|
|
182
|
+
return json.dumps(a, sort_keys=True)[:300] if isinstance(a, dict) else str(a)[:300]
|
|
183
|
+
pop, cond = "/".join(path(a["population"]) or ()), _cond(a)
|
|
184
|
+
among = f" (of {int(a['n'])})" if "n" in a else ""
|
|
185
|
+
q = a["quantifier"]
|
|
186
|
+
if q == "none":
|
|
187
|
+
return f"no {pop}{among} has {cond}"
|
|
188
|
+
if q == "all":
|
|
189
|
+
return f"every {pop}{among} has {cond}"
|
|
190
|
+
if q == "some":
|
|
191
|
+
return f"at least one {pop}{among} has {cond}"
|
|
192
|
+
return f"exactly {int(a['k'])} of {int(a['n']) if 'n' in a else 'the'} {pop} have {cond}"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def render_count(e: dict) -> str:
|
|
196
|
+
u = f" {e['unit']}" if e.get("unit") else ""
|
|
197
|
+
return (f"{e['k']} of {e['n']} {_pop(e['population'])} counted with {e['property']} "
|
|
198
|
+
f"{SHOW_OP[e['op']]} {stats_fmt(e['figure'])}{u}")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _rid(s: Snap, ref: str, inv: str) -> str:
|
|
202
|
+
if ref in s.nodes:
|
|
203
|
+
return ref
|
|
204
|
+
hits = [i for i, n in s.nodes.items() if n["type"] == "result" and n["code"].lower() == ref.lower()
|
|
205
|
+
and (not inv or n["inv"] == inv)]
|
|
206
|
+
return hits[0] if len(hits) == 1 else ref
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _decide(a: dict, e: dict) -> tuple[str, str, str]:
|
|
210
|
+
"""(verdict, field that blocked it, why) for a well-formed atom against a typed count."""
|
|
211
|
+
pa, pe = path(a["population"]), e["population"]
|
|
212
|
+
if prop(a["property"]) != e["property"]:
|
|
213
|
+
return "unresolved", "property", (f"the count is of {e['property']}, the assertion is about "
|
|
214
|
+
f"{prop(a['property'])}")
|
|
215
|
+
if not same_unit(a.get("unit"), e.get("unit")):
|
|
216
|
+
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")
|
|
218
|
+
if not (within(pa, pe) or within(pe, pa)):
|
|
219
|
+
return "unresolved", "population", (f"the count is of {_pop(pe)}, the assertion is about {_pop(pa)}: "
|
|
220
|
+
"neither lies within the other")
|
|
221
|
+
if "n" in a and int(a["n"]) != e["n"]:
|
|
222
|
+
if pa == pe:
|
|
223
|
+
return "contradicted", "n", f"the assertion says {int(a['n'])} {_pop(pa)}; the count has {e['n']}"
|
|
224
|
+
return "unresolved", "n", (f"the assertion's n ({int(a['n'])}) is the size of {_pop(pa)}, which the count of "
|
|
225
|
+
f"{_pop(pe)} ({e['n']}) doesn't give")
|
|
226
|
+
# what was counted, both ways: k inside S_e and n - k outside it
|
|
227
|
+
sides = [((e["op"], e["figure"]), e["k"]), ((COMPLEMENT[e["op"]], e["figure"]), e["n"] - e["k"])]
|
|
228
|
+
q, t = a["quantifier"], (a["op"], float(a["figure"]))
|
|
229
|
+
got = _settle(q, t, sides, pa, pe, a)
|
|
230
|
+
if got:
|
|
231
|
+
return got
|
|
232
|
+
if _settle(q, t, sides, pa, pa, a): # it would settle over the same population: the paths blocked it
|
|
233
|
+
return "unresolved", "population", (f"a count over {_pop(pe)} doesn't settle this about {_pop(pa)} (a count "
|
|
234
|
+
"of a larger population settles 'none' and 'all'; a counterexample in a "
|
|
235
|
+
"smaller one settles 'some' and refutes 'none')")
|
|
236
|
+
return "unresolved", "op", (f"the count compared {e['property']} {SHOW_OP[e['op']]} {stats_fmt(e['figure'])}, "
|
|
237
|
+
f"which settles neither way whether {render(a)}")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _settle(q: str, t: tuple, sides: list, pa: tuple, pe: tuple, a: dict) -> tuple[str, str, str] | None:
|
|
241
|
+
"""The verdict when the count settles the atom, else None."""
|
|
242
|
+
if q == "exactly":
|
|
243
|
+
if pa != pe:
|
|
244
|
+
return None
|
|
245
|
+
for side, c in sides:
|
|
246
|
+
if same_set(t, side):
|
|
247
|
+
ok = int(a["k"]) == c
|
|
248
|
+
return ("supported" if ok else "contradicted", "" if ok else "k",
|
|
249
|
+
f"the count finds {c}" + ("" if ok else f", not {int(a['k'])}"))
|
|
250
|
+
return None
|
|
251
|
+
neg = "that don't" if q == "all" else "that do"
|
|
252
|
+
if q == "all": # everything meets it: nothing meets its complement
|
|
253
|
+
q, t = "none", (COMPLEMENT[t[0]], t[1])
|
|
254
|
+
if q == "none":
|
|
255
|
+
for side, c in sides:
|
|
256
|
+
if c > 0 and subset(side, t) and within(pe, pa):
|
|
257
|
+
return "contradicted", "", f"the count finds {c} {neg}"
|
|
258
|
+
for side, c in sides:
|
|
259
|
+
if c == 0 and subset(t, side) and within(pa, pe):
|
|
260
|
+
return "supported", "", f"the count finds none {neg}"
|
|
261
|
+
else: # some
|
|
262
|
+
for side, c in sides:
|
|
263
|
+
if c > 0 and subset(side, t) and within(pe, pa):
|
|
264
|
+
return "supported", "", f"the count finds {c}"
|
|
265
|
+
for side, c in sides:
|
|
266
|
+
if c == 0 and subset(t, side) and within(pa, pe):
|
|
267
|
+
return "contradicted", "", "the count finds none"
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def verify(s: Snap, body: dict, inv: str = "") -> list[dict]:
|
|
272
|
+
"""Each atom of a claim with its verdict: supported | contradicted | unresolved, the rendered sentence, the
|
|
273
|
+
count it was decided on and, when it wasn't supported, the field and the reason."""
|
|
274
|
+
def lookup(ref: str, key: str) -> tuple[str, dict | None, int | None]:
|
|
275
|
+
rid = _rid(s, ref, inv)
|
|
276
|
+
return rid, result_value(s, rid, key), s.nodes[rid]["v"] if rid in s.nodes else None
|
|
277
|
+
return verify_with(body, lookup)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def verify_with(body: dict, lookup) -> list[dict]:
|
|
281
|
+
"""``verify`` over any store: ``lookup(result, key)`` gives (result id, the value under key or None, its
|
|
282
|
+
version). The bundle verifier passes the verified rows, so a view can't show an atom supported that its rows
|
|
283
|
+
make false."""
|
|
284
|
+
meas = body.get("measurements") or []
|
|
285
|
+
out = []
|
|
286
|
+
for i, a in enumerate(body.get("asserts") or []):
|
|
287
|
+
row = {"atom": i, "text": render(a)}
|
|
288
|
+
field, why = atom_shape(a, meas)
|
|
289
|
+
if field:
|
|
290
|
+
out.append({**row, "verdict": "unresolved", "field": field, "why": why})
|
|
291
|
+
continue
|
|
292
|
+
m = meas[int(a["measurement"])]
|
|
293
|
+
rid, rv, ver = lookup(str(m.get("result") or ""), str(m.get("key") or ""))
|
|
294
|
+
e, why = count_fields(rv)
|
|
295
|
+
row.update(result=rid, key=str(m.get("key") or ""), version=ver)
|
|
296
|
+
if e is None:
|
|
297
|
+
out.append({**row, "verdict": "unresolved", "field": "measurement", "why": why})
|
|
298
|
+
continue
|
|
299
|
+
verdict, field, why = _decide(a, e)
|
|
300
|
+
out.append({**row, "verdict": verdict, "field": field, "why": why, "k": e["k"], "n": e["n"],
|
|
301
|
+
"counted": render_count(e)})
|
|
302
|
+
return out
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def key(a) -> str:
|
|
306
|
+
"""What an atom asserts, as a stable string: the same assertion written again has the same key, whatever its
|
|
307
|
+
measurement or spelling of the unit."""
|
|
308
|
+
if atom_shape(a, None)[0]:
|
|
309
|
+
return json.dumps(a, sort_keys=True, default=str)[:300]
|
|
310
|
+
u = _unit(a.get("unit"))
|
|
311
|
+
return json.dumps([a["quantifier"], "/".join(path(a["population"]) or ()), prop(a["property"]), a["op"],
|
|
312
|
+
float(a["figure"]), sorted(u) if u else None, a.get("n"), a.get("k")], default=str)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def numbers(body: dict) -> list[float]:
|
|
316
|
+
"""The figures, k and n the atoms state: a statement may say them."""
|
|
317
|
+
out: list[float] = []
|
|
318
|
+
for a in body.get("asserts") or []:
|
|
319
|
+
if isinstance(a, dict):
|
|
320
|
+
out += [float(a[f]) for f in ("figure", "n", "k") if _num(a.get(f))]
|
|
321
|
+
return out
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# ── the drafting aid: proposes atoms from the prose and the typed counts; never trusted ────────────────────────────
|
|
325
|
+
|
|
326
|
+
def _op_of(comp: tuple | None) -> str | None:
|
|
327
|
+
if not comp:
|
|
328
|
+
return None
|
|
329
|
+
_axis, sign, strict = comp
|
|
330
|
+
return (">" if strict else ">=") if sign > 0 else ("<" if strict else "<=")
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def draft(s: Snap, body: dict, inv: str = "") -> list[dict]:
|
|
334
|
+
"""Atoms a claim's statement seems to assert, filled from its typed counts. The word checks' parsers propose them;
|
|
335
|
+
nothing proposed here is trusted — each must be written into ``asserts`` by the claimant and is then checked like
|
|
336
|
+
any other. Empty when nothing can be proposed."""
|
|
337
|
+
from . import checks as C
|
|
338
|
+
from . import language as L
|
|
339
|
+
stmt = body.get("statement", "") or ""
|
|
340
|
+
meas = body.get("measurements") or []
|
|
341
|
+
typed = []
|
|
342
|
+
for i, m in enumerate(meas):
|
|
343
|
+
if isinstance(m, dict):
|
|
344
|
+
rid = _rid(s, str(m.get("result") or ""), inv)
|
|
345
|
+
e, _ = count_fields(result_value(s, rid, str(m.get("key") or "")))
|
|
346
|
+
if e:
|
|
347
|
+
typed.append((i, e))
|
|
348
|
+
if not typed:
|
|
349
|
+
return []
|
|
350
|
+
out: list[dict] = []
|
|
351
|
+
|
|
352
|
+
def add(a: dict, e: dict) -> None:
|
|
353
|
+
if e.get("unit"):
|
|
354
|
+
a["unit"] = e["unit"]
|
|
355
|
+
if a not in out:
|
|
356
|
+
out.append(a)
|
|
357
|
+
|
|
358
|
+
for part in C.asserting_parts(stmt): # one clause at a time: its own quantifier, its own figures
|
|
359
|
+
said = [w.lower() for w in L.asserted(L.ABSOLUTE, part)]
|
|
360
|
+
if said:
|
|
361
|
+
q = "all" if any(C._ALL.match(w) for w in said) else "none"
|
|
362
|
+
for fig, comp in C._clause_comparisons(part, after_no=True):
|
|
363
|
+
op = _op_of(comp)
|
|
364
|
+
if op is None:
|
|
365
|
+
continue
|
|
366
|
+
# the counts made at this figure, when there are any: the figure is the one fact the clause and
|
|
367
|
+
# the count share without reading what either is about
|
|
368
|
+
pool = [(i, e) for i, e in typed if e["figure"] == fig] or typed
|
|
369
|
+
for i, e in pool:
|
|
370
|
+
add({"quantifier": q, "population": _pop(e["population"]), "property": e["property"], "op": op,
|
|
371
|
+
"figure": int(fig) if float(fig).is_integer() else fig, "n": e["n"], "measurement": i}, e)
|
|
372
|
+
figs = {v for v, *_ in C.numbers(part)}
|
|
373
|
+
for k, n in ((int(a.replace(",", "")), int(b.replace(",", ""))) for a, b in C._STATED_COUNT.findall(part)):
|
|
374
|
+
# what the clause says, true or not — the check decides; counts at a figure the clause names first
|
|
375
|
+
pool = [(i, e) for i, e in typed if e["n"] == n]
|
|
376
|
+
for i, e in [x for x in pool if x[1]["figure"] in figs] or pool:
|
|
377
|
+
add({"quantifier": "exactly", "population": _pop(e["population"]), "property": e["property"],
|
|
378
|
+
"op": e["op"], "figure": int(e["figure"]) if e["figure"].is_integer() else e["figure"],
|
|
379
|
+
"k": k, "n": n, "measurement": i}, e)
|
|
380
|
+
return out[:6]
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
__all__ = ["COMPLEMENT", "OPS", "QUANTIFIERS", "atom_shape", "count_fields", "draft", "holds", "key", "numbers", "path",
|
|
384
|
+
"render", "render_count", "same_set", "same_unit", "subset", "verify", "verify_with", "within"]
|
|
@@ -33,7 +33,7 @@ import os
|
|
|
33
33
|
from pathlib import Path
|
|
34
34
|
from typing import TYPE_CHECKING, Any
|
|
35
35
|
|
|
36
|
-
from . import __version__
|
|
36
|
+
from . import __version__, assertions
|
|
37
37
|
from .schema import RELS, SAME_TYPE
|
|
38
38
|
from .store import GENESIS, ROW_TABLES, check_rows, event_body, now
|
|
39
39
|
|
|
@@ -349,8 +349,9 @@ def _derived_problem(b: dict, tables: dict, historical: list | None = None, unco
|
|
|
349
349
|
redacted: int = 0) -> str:
|
|
350
350
|
"""The derived views are presentation, but a viewer shows them beside a verified chain: every fact in them that
|
|
351
351
|
the rows fix — which investigations there are, which nodes and live links each has, each node's type, code,
|
|
352
|
-
author, time, version, title and lifecycle, each link's ends and fields
|
|
353
|
-
|
|
352
|
+
author, time, version, title and lifecycle, each link's ends and fields, and each typed assertion's verdict (a
|
|
353
|
+
function of the claim's and its results' rows, assertions.py) — must be present and agree with the verified
|
|
354
|
+
rows. A view that leaves a fact out fails: a viewer would fill the gap with a default (a withdrawn claim
|
|
354
355
|
shown as active). The rest of standing can't be checked this way.
|
|
355
356
|
|
|
356
357
|
A lifecycle is checked against the current rule only. One that matches only 0.3's rule (the end of a chain of
|
|
@@ -414,6 +415,17 @@ def _derived_problem(b: dict, tables: dict, historical: list | None = None, unco
|
|
|
414
415
|
st = x.get("state")
|
|
415
416
|
if not isinstance(st, dict) or "lifecycle" not in st:
|
|
416
417
|
return f"{where} shows {name} without its lifecycle"
|
|
418
|
+
if n["type"] == "claim" and body.get("asserts"):
|
|
419
|
+
def lookup(ref: str, key: str) -> tuple:
|
|
420
|
+
ver, rb = latest.get(ref, (None, {}))
|
|
421
|
+
rv = (rb.get("values") or {}).get(key) if ref in nodes and nodes[ref]["type"] == "result" else None
|
|
422
|
+
rv = {"value": rv} if isinstance(rv, (int, float)) and not isinstance(rv, bool) else rv
|
|
423
|
+
return ref, rv if isinstance(rv, dict) else None, ver
|
|
424
|
+
want_v = [a["verdict"] for a in assertions.verify_with(body, lookup)]
|
|
425
|
+
got_v = [a.get("verdict") if isinstance(a, dict) else None for a in st.get("assertions") or []]
|
|
426
|
+
if got_v != want_v:
|
|
427
|
+
return (f"{where} shows {name}'s typed assertions as {_j(got_v)}; its rows make them {_j(want_v)}"
|
|
428
|
+
if got_v else f"{where} shows {name} without its typed assertions")
|
|
417
429
|
w, shown = lifecycle(n["id"], True), st["lifecycle"]
|
|
418
430
|
if shown == w:
|
|
419
431
|
return ""
|