hypgraph 0.5.1__tar.gz → 0.5.3__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.5.1 → hypgraph-0.5.3}/CHANGELOG.md +42 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/PKG-INFO +24 -14
- {hypgraph-0.5.1 → hypgraph-0.5.3}/README.md +23 -13
- {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/profile-workflows-plan.md +1 -1
- {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/spec.md +6 -1
- {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/workflows/knowledge.md +5 -1
- {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/workflows/optimization.md +4 -1
- {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/workflows/software.md +6 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/workflows/trading.md +4 -2
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/__init__.py +1 -1
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/derive.py +112 -2
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/plan.py +13 -6
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/report_check.py +276 -2
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/workflow.py +52 -14
- hypgraph-0.5.3/tests/test_report_answers.py +184 -0
- hypgraph-0.5.3/tests/test_unnamed_inputs.py +94 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_workflows.py +98 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/.gitignore +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/AGENTS.md +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/CITATION.cff +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/LICENSE +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/NOTICE +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/PLAN.md +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/decisions/ADR-002-governance-profiles.md +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/review-2026-10-24.md +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/workflows/README.md +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/workflows/scientific.md +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/pyproject.toml +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/__main__.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/bundle.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/checks.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/cli.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/coord.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/demos/__init__.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/demos/broad_street.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/graph.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/language.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/mcp.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/policy.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/profiles.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/refusals.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/schema.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/server.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/skill/SKILL.md +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/stats.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/store.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/ui/__init__.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/ui/atlas.css +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/ui/atlas.js +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/ui/hypgraph.svg +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/ui/index.html +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/ui/vendor/cose-base.js +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/ui/vendor/layout-base.js +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/studies/harmony/tests/test_suite.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_cancel_releases_lease.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_coord.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_d10.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_engine.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_engine_fixes.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_incremental.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_log_and_check.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_order_detail.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_profiles.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_profiles_041.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_questions_not_assertions.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_refusals.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_server_auth.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_stats_values.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_surfaces.py +0 -0
- {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_vocabulary_friction.py +0 -0
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.3 — 2026-10-26: results that name no file can go stale
|
|
4
|
+
|
|
5
|
+
- **A result that names no file it read can go stale.** Under a workflow (`@2` profiles), a result whose run has no
|
|
6
|
+
`uses` link was fresh forever, however the files changed. It now goes STALE once an input file of its
|
|
7
|
+
investigation is replaced or withdrawn after the result was written ("it names no file it read, and A1 changed
|
|
8
|
+
after it was written"), and claims resting only on it with it. A file registered on another question only
|
|
9
|
+
(`part_of`) doesn't count against it; run outputs and page excerpts never do; a result written after the change
|
|
10
|
+
is fresh; a result that names its files keeps the usual rule (only those files count). Ordered by the event log,
|
|
11
|
+
not timestamps. Records without a workflow are unchanged. Measured on 851 results in HypSwarm's stored study
|
|
12
|
+
records: none changes standing.
|
|
13
|
+
|
|
14
|
+
## 0.5.2 — 2026-10-26: freshness for sources, stale evidence in the report
|
|
15
|
+
|
|
16
|
+
For P3 (the change suite and the instruments in HypSwarm), the record had to know when a *page* moved, not only a
|
|
17
|
+
file:
|
|
18
|
+
- **A source is fresh while its page still says it.** A host that re-fetches a source's page registers an
|
|
19
|
+
`excerpt` artifact (`derived_from` the source; `meta.contains_passage`, `meta.fetched`). The source is STALE when
|
|
20
|
+
every excerpt of it was superseded, or the live one says the page no longer contains its passage — an edit
|
|
21
|
+
elsewhere on the page keeps it fresh. A source never re-fetched is not stale: nothing says it moved.
|
|
22
|
+
- **A claim is stale when every result and every cited source behind it is.** The knowledge gates
|
|
23
|
+
(`source_quoted`, `sourced`) take only fresh sources, so a changed page reopens the question at *sourced*.
|
|
24
|
+
- **The report marks it.** Claims and results on stale evidence read `checked · STALE` in the facts tables, a new
|
|
25
|
+
open item lists them, the workflow section lists stale sources too, and the report check treats calling a stale
|
|
26
|
+
claim settled as `REPORT_OVERSTATES_STANDING`.
|
|
27
|
+
- **Gates count only what still speaks for the question.** `result_exists`, `backtested` and `solved` take only
|
|
28
|
+
fresh results (their message says which went stale and why); `result_reviewed` doesn't count a check of a stale
|
|
29
|
+
result; a decision is stale when everything it was based on is, and `positioned` / `decided` take only fresh ones
|
|
30
|
+
— a position taken on a backtest the vendor later corrected has to be decided again.
|
|
31
|
+
- **The report check reads the answers block.** A fenced ```answers block (the part of a report a harness scores and a
|
|
32
|
+
reader copies) was skipped like any code block, so a report could carry an answer only stale evidence gave and pass.
|
|
33
|
+
Now each answer has to be one a current claim on its question makes — live, not stale — at the precision written and
|
|
34
|
+
with its sign: `REPORT_ANSWER_STALE` when only old claims or stale results give it (or a current claim binds the
|
|
35
|
+
same field to another value), `REPORT_ANSWER_UNBACKED` when no claim does or the question has no claim,
|
|
36
|
+
`REPORT_ANSWERS_UNREADABLE` for invalid JSON. Results count through the claims that bind them, and a count k of n
|
|
37
|
+
backs its rate; a number a claim names only as a superseded figure doesn't back an answer; URLs, paths and hashes
|
|
38
|
+
are provenance. Keys are question codes in any case, or a flat object checked against the investigation. Found by
|
|
39
|
+
HypSwarm's change suite.
|
|
40
|
+
- **The report check skips the record's own facts.** A line inside a "Facts from the research graph" block is not
|
|
41
|
+
linted when it is exactly a line the record generates now (a hypothesis that says "significantly" is a conjecture
|
|
42
|
+
the record lists, not the author's assertion). Anything else written under that heading is checked as usual.
|
|
43
|
+
Found when a report was refused for the hypothesis line the record itself had written.
|
|
44
|
+
|
|
3
45
|
## 0.5.1 — 2026-10-26
|
|
4
46
|
|
|
5
47
|
What wiring HypSwarm's scheduler to the stages (P2) found the workflows had no honest ending or no right answer for:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hypgraph
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
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/
|
|
@@ -135,21 +135,31 @@ team, and they can verify it without trusting you.
|
|
|
135
135
|
## One record, five disciplines
|
|
136
136
|
|
|
137
137
|
The rules above are the scientific case written as code. A **profile** says what a domain means by evidence,
|
|
138
|
-
acceptance and done — and
|
|
139
|
-
|
|
138
|
+
acceptance and done — and, from 0.5, carries the discipline's own **workflow**: the stages a question passes
|
|
139
|
+
through, the gate out of each, what invalidates evidence, the typed shapes. The record enforces all of it, so none
|
|
140
|
+
of it depends on a prompt being followed. Pin one on the investigation (`"profile": "software@2"`); without one, the
|
|
141
|
+
record behaves exactly as before.
|
|
140
142
|
|
|
141
|
-
| profile |
|
|
143
|
+
| profile | the stages a question passes through | the record refuses | done when |
|
|
142
144
|
|---|---|---|---|
|
|
143
|
-
| `scientific@
|
|
144
|
-
| `software@
|
|
145
|
-
| `trading@
|
|
146
|
-
| `knowledge@
|
|
147
|
-
| `optimization@
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
145
|
+
| `scientific@2` | asked → conjectured → preregistered (**before** any run) → run → evidenced → claimed → reviewed | numbers no result accounts for, "never" without the count, "significant" without the test, a check on stale evidence | every question settled |
|
|
146
|
+
| `software@2` | specified → implemented (a diff by hash) → verified (**the instrument ran the tests on that head**) → claimed → reviewed | *"it passes"* without an instrument run, or on a superseded head; the diff's author as its reviewer | every requirement's claim rests on a passing run on the current head |
|
|
147
|
+
| `trading@2` | asked → thesised → preregistered → backtested (**out of sample, after costs, trials declared**) → claimed → risk-reviewed → positioned → monitored | a backtest without its costs; *"edge"* without both, or without the configurations tried; a position without size, stop, review date; any live order | questions settled, a position taken or declined, nothing past its review date |
|
|
148
|
+
| `knowledge@2` | asked → (searched) → sourced (**the passage**, retrieved, kind) → claimed → checked | a claim with no source; a source without its passage | every claim cites a source and is checked |
|
|
149
|
+
| `optimization@2` | posed (the instance by hash) → modelled → configured → solved → verified → claimed → checked | an objective without feasibility; *"optimal"* without a bound, or on a changed instance; a solve that uses no instance | a feasible bounded incumbent, verified on the current instance |
|
|
150
|
+
|
|
151
|
+
**The stage is derived, never set**: `g.stage(question)` reads it off the record (the first stage whose gate is not
|
|
152
|
+
met, every gate MET / NOT MET with what would meet it); `GET /api/graph/stages`; MCP tool `stage`. A workflow
|
|
153
|
+
widens what puts dependants AT RISK — `uses` (a corrected data file, a superseded diff or instance), `cites` (a
|
|
154
|
+
withdrawn source) — and evidence goes **stale** when the artifact it used is superseded or its `valid_until` has
|
|
155
|
+
passed. A result whose run names no file it read goes stale when an input file of its question changes after it:
|
|
156
|
+
nothing shows it read the new version (name the files with `uses` and only those count). An honest "it can't be answered" (a checked `inconclusive` claim), a declined thesis (`no_position`) and a
|
|
157
|
+
verified failure each complete a question the gates alone never would.
|
|
158
|
+
|
|
159
|
+
Profiles are versioned and frozen: a record written under `trading@1` means the same thing in a year, and the `@1`
|
|
160
|
+
profiles stay exactly as they were. `completion(inv)` reports each criterion; `hypgraph serve` exposes
|
|
161
|
+
`/api/graph/completion` and `/api/graph/profiles`. The method each workflow is taken from is quoted, one page each,
|
|
162
|
+
in [docs/workflows/](docs/workflows/). Adding a discipline is one `Profile` with a `Workflow`.
|
|
153
163
|
|
|
154
164
|
## Connect your agent
|
|
155
165
|
|
|
@@ -103,21 +103,31 @@ team, and they can verify it without trusting you.
|
|
|
103
103
|
## One record, five disciplines
|
|
104
104
|
|
|
105
105
|
The rules above are the scientific case written as code. A **profile** says what a domain means by evidence,
|
|
106
|
-
acceptance and done — and
|
|
107
|
-
|
|
106
|
+
acceptance and done — and, from 0.5, carries the discipline's own **workflow**: the stages a question passes
|
|
107
|
+
through, the gate out of each, what invalidates evidence, the typed shapes. The record enforces all of it, so none
|
|
108
|
+
of it depends on a prompt being followed. Pin one on the investigation (`"profile": "software@2"`); without one, the
|
|
109
|
+
record behaves exactly as before.
|
|
108
110
|
|
|
109
|
-
| profile |
|
|
111
|
+
| profile | the stages a question passes through | the record refuses | done when |
|
|
110
112
|
|---|---|---|---|
|
|
111
|
-
| `scientific@
|
|
112
|
-
| `software@
|
|
113
|
-
| `trading@
|
|
114
|
-
| `knowledge@
|
|
115
|
-
| `optimization@
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
| `scientific@2` | asked → conjectured → preregistered (**before** any run) → run → evidenced → claimed → reviewed | numbers no result accounts for, "never" without the count, "significant" without the test, a check on stale evidence | every question settled |
|
|
114
|
+
| `software@2` | specified → implemented (a diff by hash) → verified (**the instrument ran the tests on that head**) → claimed → reviewed | *"it passes"* without an instrument run, or on a superseded head; the diff's author as its reviewer | every requirement's claim rests on a passing run on the current head |
|
|
115
|
+
| `trading@2` | asked → thesised → preregistered → backtested (**out of sample, after costs, trials declared**) → claimed → risk-reviewed → positioned → monitored | a backtest without its costs; *"edge"* without both, or without the configurations tried; a position without size, stop, review date; any live order | questions settled, a position taken or declined, nothing past its review date |
|
|
116
|
+
| `knowledge@2` | asked → (searched) → sourced (**the passage**, retrieved, kind) → claimed → checked | a claim with no source; a source without its passage | every claim cites a source and is checked |
|
|
117
|
+
| `optimization@2` | posed (the instance by hash) → modelled → configured → solved → verified → claimed → checked | an objective without feasibility; *"optimal"* without a bound, or on a changed instance; a solve that uses no instance | a feasible bounded incumbent, verified on the current instance |
|
|
118
|
+
|
|
119
|
+
**The stage is derived, never set**: `g.stage(question)` reads it off the record (the first stage whose gate is not
|
|
120
|
+
met, every gate MET / NOT MET with what would meet it); `GET /api/graph/stages`; MCP tool `stage`. A workflow
|
|
121
|
+
widens what puts dependants AT RISK — `uses` (a corrected data file, a superseded diff or instance), `cites` (a
|
|
122
|
+
withdrawn source) — and evidence goes **stale** when the artifact it used is superseded or its `valid_until` has
|
|
123
|
+
passed. A result whose run names no file it read goes stale when an input file of its question changes after it:
|
|
124
|
+
nothing shows it read the new version (name the files with `uses` and only those count). An honest "it can't be answered" (a checked `inconclusive` claim), a declined thesis (`no_position`) and a
|
|
125
|
+
verified failure each complete a question the gates alone never would.
|
|
126
|
+
|
|
127
|
+
Profiles are versioned and frozen: a record written under `trading@1` means the same thing in a year, and the `@1`
|
|
128
|
+
profiles stay exactly as they were. `completion(inv)` reports each criterion; `hypgraph serve` exposes
|
|
129
|
+
`/api/graph/completion` and `/api/graph/profiles`. The method each workflow is taken from is quoted, one page each,
|
|
130
|
+
in [docs/workflows/](docs/workflows/). Adding a discipline is one `Profile` with a `Workflow`.
|
|
121
131
|
|
|
122
132
|
## Connect your agent
|
|
123
133
|
|
|
@@ -95,7 +95,7 @@ instrument, mechanical feasibility recomputation (P3). **P2 built in HypSwarm 0.
|
|
|
95
95
|
orders, review on the node the previous gate produced, the record's word on done; the examples' scripted models walk
|
|
96
96
|
the stages; the discovery stub run through its stages). P2 fed back into hypgraph 0.5.1: honest endings the stages
|
|
97
97
|
lacked (a declined thesis, an inconclusive answer, a verified failure), artifacts that learn their kind on
|
|
98
|
-
re-registration, optimization's `checked` stage. P3
|
|
98
|
+
re-registration, optimization's `checked` stage. **P3 built (2026-10-26):** the instruments in HypSwarm 0.1.0a5 (world watcher, re-verification, source re-fetch, refresh orders) and hypgraph 0.5.2 (source freshness, fresh-only gates, stale in the report); the change suite in HypSwarm `studies/discovery/PLAN-CHANGE.md`, self-test and stub run at $0, predictions locked; the model run waits for the spend approval.
|
|
99
99
|
|
|
100
100
|
## 4. What stays as it is, on purpose
|
|
101
101
|
|
|
@@ -209,7 +209,9 @@ A report aggregates per author (refused, fixed, open, codes) and per rule.
|
|
|
209
209
|
## 9. Checking a write-up against the record
|
|
210
210
|
|
|
211
211
|
A checker reads markdown sentence by sentence and checks it against the record. Code blocks, inline code, URLs, link
|
|
212
|
-
targets and HTML comments are skipped
|
|
212
|
+
targets and HTML comments are skipped, except a fenced block whose info string is `answers`: that is the report's
|
|
213
|
+
machine-read answer (a JSON object, keyed by question code or flat), and every number or true / false in it is checked
|
|
214
|
+
against the claims. Quoted passages and negated words ("cannot be proven") are not treated as
|
|
213
215
|
the writer's assertion. Codes are matched within the investigation being reported on.
|
|
214
216
|
|
|
215
217
|
| level | code | when |
|
|
@@ -220,6 +222,9 @@ the writer's assertion. Codes are matched within the investigation being reporte
|
|
|
220
222
|
| error | `REPORT_ABSOLUTE_UNBACKED` | never / always / impossible / guaranteed / eliminates / proven / 100% asserted, not in an instruction, with no cited claim carrying a count with k ∈ {0, n} |
|
|
221
223
|
| error | `REPORT_ABSENCE_UNBACKED` | "no effect" / "rules out" / "doesn't matter" without citing a claim filed as `exclusion` or `inconclusive` |
|
|
222
224
|
| error | `REPORT_SIGNIFICANCE_UNBACKED` | "significantly" without a p-value or interval in the sentence and without citing a node that carries a test (a negated "not significant" is a warning) |
|
|
225
|
+
| error | `REPORT_ANSWER_STALE` | a value in the `answers` block that only stale, superseded or withdrawn claims (or stale results) on that question give, or that a current claim binds to a different value under the same field while an old one binds it to this; or a question whose every claim is old |
|
|
226
|
+
| error | `REPORT_ANSWER_UNBACKED` | a value in the `answers` block that no current claim on the question states or binds, at the precision the answer gives it and with its sign (a count k of n, bound or written as "k of n", backs its rate); or a question answered with no claim at all. A number a claim names only as a figure that no longer holds ("the earlier count of 789 is superseded") doesn't back it. URLs, paths and hashes in the block are provenance and aren't checked. |
|
|
227
|
+
| error | `REPORT_ANSWERS_UNREADABLE` | the `answers` block isn't valid JSON |
|
|
223
228
|
| warning | `REPORT_UNSOURCED_NUMBER` | a number not contained in or implied by the investigation's results, live claims' measurements, statements and scopes, the investigation's scope and constants, or experiment and run settings. Integers ≤ 12, years, dates, times and 7-digit+ ids are skipped. Statistical quantities need one decimal. |
|
|
224
229
|
| warning | `REPORT_OMITS_DISPUTE` | cites a contested or at-risk node without saying so |
|
|
225
230
|
| warning | `REPORT_CITES_SUPERSEDED` | cites a superseded node without saying so |
|
|
@@ -73,7 +73,11 @@ review.
|
|
|
73
73
|
|
|
74
74
|
1. *Is `searched` required?* **Optional** (reported, never blocks); a task that wants a systematic answer puts
|
|
75
75
|
`search_recorded` in its `done_when`.
|
|
76
|
-
2. *The re-fetch check?* **P3
|
|
76
|
+
2. *The re-fetch check?* **Built in P3** (hypgraph 0.5.2 + HypSwarm 0.1.0a5): a host's `refetch(url)` files each
|
|
77
|
+
source's page as an `excerpt` artifact with `contains_passage`; a page that no longer says the passage makes the
|
|
78
|
+
source and the claims citing it stale, and the question returns to *sourced*. `NUMBER_NOT_IN_PASSAGE` is not a
|
|
79
|
+
separate refusal: a claim number that is in no bound result and not in the cited passage is already
|
|
80
|
+
`UNBOUND_NUMBER`.
|
|
77
81
|
3. *Can the fact-checker re-kind a source?* **Yes**: `corrects` now joins sources; the correcting source carries
|
|
78
82
|
the kind, and the old one is superseded.
|
|
79
83
|
|
|
@@ -72,7 +72,10 @@ recomputed feasibility.
|
|
|
72
72
|
|
|
73
73
|
1. *The model: hypothesis or experiment?* **Hypothesis** (`modelled`, gate `hypothesis_stated`): it can be wrong
|
|
74
74
|
independently of the solve and is revisable with its own version.
|
|
75
|
-
2. *Mechanical feasibility recomputation?* **P3
|
|
75
|
+
2. *Mechanical feasibility recomputation?* **Built in P3 as re-execution**: the instrument re-runs the solver's
|
|
76
|
+
registered code on the instance as it is now (a changed instance is a new artifact that supersedes the old), so
|
|
77
|
+
objective, feasibility and bound are recomputed mechanically; a domain-specific `check_solution` reading a
|
|
78
|
+
solution artifact stays a host hook for later. Built in P1: `result_reviewed` —
|
|
76
79
|
the verifier's check on the result, by someone who is not its author — and `incumbent_verified` needs it on the
|
|
77
80
|
current instance.
|
|
78
81
|
3. *Sensitivity?* **An optional `sensitivity` field on the decision.**
|
|
@@ -77,6 +77,12 @@ superseded heads, listed as superseded); open requirements.
|
|
|
77
77
|
|
|
78
78
|
Also decided: `REVIEW_BY_AUTHOR` covers the diff's author (built); `supersedes` joins artifacts (built).
|
|
79
79
|
|
|
80
|
+
**CI as the instrument (P3, HypSwarm 0.1.0a5).** `run(..., watch=True)` keeps the working tree of the task's
|
|
81
|
+
files as one `kind=diff` artifact that every commit supersedes; a run under an order uses the question's current
|
|
82
|
+
head; when a head moves, the instrument re-runs every stale result's command on the new tree — the same values bring
|
|
83
|
+
the claim back with no model call, a moved count leaves it stale and the question is redone. That is the CI rule
|
|
84
|
+
("tests on this commit"), kept by the record rather than by a pipeline.
|
|
85
|
+
|
|
80
86
|
**Verified failure (0.5.1).** When the instrument's run on the current head records a negative result ("1 of 2
|
|
81
87
|
checks pass"), `tests_pass_current` can never be met; a checked claim resting on that result settles the
|
|
82
88
|
requirement as a verified failure (`stage()` says so), and `requirements_verified_current` stays unmet. The record
|
|
@@ -70,8 +70,10 @@ passed without a newer result.
|
|
|
70
70
|
|
|
71
71
|
## Review questions and decisions (2026-10-26)
|
|
72
72
|
|
|
73
|
-
1. *Will agents report `trials` honestly?* **Required
|
|
74
|
-
need
|
|
73
|
+
1. *Will agents report `trials` honestly?* **Required, and checked like any value**: `backtested` and
|
|
74
|
+
`EDGE_WITHOUT_TRIALS` need it, and the instrument's re-run confirms or contradicts the printed `trials` as it does
|
|
75
|
+
every registered value. Counting a parameter grid statically from arbitrary code was not built (P3): it can't be
|
|
76
|
+
done reliably, and a wrong count would be worse than the confirmed one.
|
|
75
77
|
2. *`review_date` default?* **Required, no default** (`POSITION_WITHOUT_STOP` names what is missing).
|
|
76
78
|
3. *Who raises a regime notice?* **The host or the risk seat**: it is a withdrawal (or supersession) of the data
|
|
77
79
|
artifact, which only they write; `uses` then carries it to every position.
|
|
@@ -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.5.
|
|
19
|
+
__version__ = "0.5.3"
|
|
20
20
|
|
|
21
21
|
from .bundle import verify_bundle # noqa: E402
|
|
22
22
|
from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
|
|
@@ -146,6 +146,14 @@ def risky(s: Snap, x: str) -> tuple:
|
|
|
146
146
|
return RISKY + extra if extra else RISKY
|
|
147
147
|
|
|
148
148
|
|
|
149
|
+
def workflow_profile(s: Snap, x: str) -> bool:
|
|
150
|
+
"""Whether x's investigation runs a profile with a workflow (the @2 profiles): freshness rules that need the files
|
|
151
|
+
to be registered apply only there."""
|
|
152
|
+
inv = s.nodes.get(s.nodes[x]["inv"]) if x in s.nodes else None
|
|
153
|
+
pid = str((inv or {}).get("body", {}).get("profile") or "").lower()
|
|
154
|
+
return bool(pid) and pid in EXTRA_RISKY
|
|
155
|
+
|
|
156
|
+
|
|
149
157
|
def upstream(s: Snap, x: str) -> list[tuple[str, str]]:
|
|
150
158
|
"""What x rests on, one step: premises (requires), derivations, a decision's basis, and the run that produced it.
|
|
151
159
|
Support is counted separately (unsupported): one fallen support among several doesn't break a claim."""
|
|
@@ -163,11 +171,90 @@ def downstream(s: Snap, x: str) -> list[tuple[str, str]]:
|
|
|
163
171
|
return downs
|
|
164
172
|
|
|
165
173
|
|
|
174
|
+
def excerpts(s: Snap, sid: str) -> list[str]:
|
|
175
|
+
"""The pages a host fetched for a source: artifacts ``kind=excerpt`` derived_from it (oldest first)."""
|
|
176
|
+
return [e["src"] for e in s.inn.get(sid, []) if e["rel"] == "derived_from"
|
|
177
|
+
and s.nodes[e["src"]]["type"] == "artifact" and (s.nodes[e["src"]]["body"].get("kind") or "") == "excerpt"]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _order(s: Snap) -> tuple[dict, dict, dict]:
|
|
181
|
+
"""When each node was created, each edge linked and each node withdrawn, as event sequence numbers (timestamps
|
|
182
|
+
are only to the second). Cached on the snapshot until the next event."""
|
|
183
|
+
cached = getattr(s, "_order_cache", None)
|
|
184
|
+
if cached and cached[0] == s.seq:
|
|
185
|
+
return cached[1]
|
|
186
|
+
made: dict[str, int] = {}
|
|
187
|
+
linked: dict[int, int] = {}
|
|
188
|
+
gone: dict[str, int] = {}
|
|
189
|
+
for e in s.events:
|
|
190
|
+
if e["kind"] == "create":
|
|
191
|
+
made.setdefault(e["ref"], e["seq"])
|
|
192
|
+
elif e["kind"] == "link" and isinstance(e.get("payload"), dict) and e["payload"].get("edge") is not None:
|
|
193
|
+
linked[int(e["payload"]["edge"])] = e["seq"]
|
|
194
|
+
elif e["kind"] == "withdraw":
|
|
195
|
+
gone.setdefault(e["ref"], e["seq"])
|
|
196
|
+
out = (made, linked, gone)
|
|
197
|
+
s._order_cache = (s.seq, out) # type: ignore[attr-defined]
|
|
198
|
+
return out
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _moved_since(s: Snap, rid: str) -> list[str]:
|
|
202
|
+
"""Input files of the result's investigation that were replaced or withdrawn after the result was written. A run
|
|
203
|
+
output (an artifact some run produced) and a page excerpt (a re-fetch supersedes it every time) don't count, and a
|
|
204
|
+
file registered on other questions only (``part_of``) doesn't count against a result on a different one."""
|
|
205
|
+
from .workflow import question_of # workflow imports derive; read lazily
|
|
206
|
+
made, linked, gone = _order(s)
|
|
207
|
+
t = made.get(rid)
|
|
208
|
+
if t is None:
|
|
209
|
+
return []
|
|
210
|
+
inv = s.nodes[rid]["inv"]
|
|
211
|
+
q: set | None = None
|
|
212
|
+
out = []
|
|
213
|
+
for aid, a in s.nodes.items():
|
|
214
|
+
if a["type"] != "artifact" or a["inv"] != inv or (a["body"].get("kind") or "") == "excerpt":
|
|
215
|
+
continue
|
|
216
|
+
if any(e["rel"] == "produced" for e in s.inn.get(aid, [])):
|
|
217
|
+
continue
|
|
218
|
+
on = {e["dst"] for e in s.out.get(aid, []) if e["rel"] == "part_of" and s.nodes[e["dst"]]["type"] == "question"}
|
|
219
|
+
if on:
|
|
220
|
+
if q is None:
|
|
221
|
+
# a result reaches its question through the claims built on it, as often as through its run
|
|
222
|
+
qs = {question_of(s, rid)} | {question_of(s, e["src"]) for e in s.inn.get(rid, [])
|
|
223
|
+
if e["rel"] == "derived_from"}
|
|
224
|
+
qs |= {question_of(s, e["dst"]) for e in s.out.get(rid, []) if e["rel"] == "supports"}
|
|
225
|
+
q = {x for x in qs if x}
|
|
226
|
+
if q and not (q & on):
|
|
227
|
+
continue
|
|
228
|
+
when = [linked.get(e["id"], 0) for e in s.inn.get(aid, []) if e["rel"] == "supersedes"]
|
|
229
|
+
if aid in gone:
|
|
230
|
+
when.append(gone[aid])
|
|
231
|
+
if any(w > t for w in when):
|
|
232
|
+
out.append(a["code"] or aid)
|
|
233
|
+
return out
|
|
234
|
+
|
|
235
|
+
|
|
166
236
|
def stale(s: Snap, nid: str) -> list[str]:
|
|
167
237
|
"""Why a result (or a claim through its results) no longer speaks for what it measured; [] when fresh. A result
|
|
168
238
|
is stale when the run that produced it used an artifact that is now superseded or withdrawn, or when its
|
|
169
|
-
``valid_until`` has passed.
|
|
239
|
+
``valid_until`` has passed. Under a workflow, a result whose run names no file it read (no ``uses``) is stale once
|
|
240
|
+
any input file of its investigation is replaced or withdrawn after it was written: nothing shows it read the
|
|
241
|
+
new one. A source is stale when the host re-fetched its page and the page as it is now no
|
|
242
|
+
longer contains its passage (the live excerpt says ``contains_passage: false``), or every excerpt of it was
|
|
243
|
+
superseded. A claim is stale when it has results or cited sources and every one of them is stale."""
|
|
170
244
|
n = s.nodes[nid]
|
|
245
|
+
if n["type"] == "source":
|
|
246
|
+
ex = excerpts(s, nid)
|
|
247
|
+
if not ex:
|
|
248
|
+
return [] # never re-fetched: nothing says the page moved
|
|
249
|
+
live = [a for a in ex if lifecycle(s, a) == "active"]
|
|
250
|
+
if not live:
|
|
251
|
+
return [f"its page changed since it was read ({', '.join(s.nodes[a]['code'] or a for a in ex[-2:])} "
|
|
252
|
+
"superseded)"]
|
|
253
|
+
meta = [s.nodes[a]["body"].get("meta") or {} for a in live]
|
|
254
|
+
if all(m.get("contains_passage") is False for m in meta):
|
|
255
|
+
when = max((str(m.get("fetched") or "") for m in meta), default="")
|
|
256
|
+
return [f"the page no longer contains its passage (re-fetched{' ' + when if when else ''})"]
|
|
257
|
+
return []
|
|
171
258
|
if n["type"] == "result":
|
|
172
259
|
out = []
|
|
173
260
|
run = producing_run(s, nid)
|
|
@@ -179,15 +266,29 @@ def stale(s: Snap, nid: str) -> list[str]:
|
|
|
179
266
|
a = s.nodes[e["dst"]]
|
|
180
267
|
kind = a["body"].get("kind") or "artifact"
|
|
181
268
|
out.append(f"{kind} {a['code'] or e['dst']} is {lc}")
|
|
269
|
+
named = run and any(e["rel"] == "uses" for e in s.out.get(run, []))
|
|
270
|
+
if not named and workflow_profile(s, nid):
|
|
271
|
+
moved = _moved_since(s, nid)
|
|
272
|
+
if moved:
|
|
273
|
+
out.append(f"it names no file it read, and {', '.join(moved[:3])} changed after it was written")
|
|
182
274
|
vu = str(n["body"].get("valid_until") or "").strip()
|
|
183
275
|
if vu and s.now and s.now[:len(vu)] > vu:
|
|
184
276
|
out.append(f"valid_until {vu} has passed")
|
|
185
277
|
return out
|
|
278
|
+
if n["type"] == "decision":
|
|
279
|
+
# a decision is stale when everything it was based on is (a position taken on a backtest the vendor corrected)
|
|
280
|
+
basis = [e["dst"] for e in s.out.get(nid, []) if e["rel"] == "based_on" and not fallen(s, e["dst"])]
|
|
281
|
+
reasons = [stale(s, b) for b in basis]
|
|
282
|
+
if basis and all(reasons):
|
|
283
|
+
return list(dict.fromkeys(x for r in reasons for x in r))
|
|
284
|
+
return []
|
|
186
285
|
if n["type"] == "claim":
|
|
187
286
|
rs = [e["src"] for e in s.inn.get(nid, []) if e["rel"] == "supports" and s.nodes[e["src"]]["type"] == "result"
|
|
188
287
|
and not fallen(s, e["src"])]
|
|
189
288
|
rs += [e["dst"] for e in s.out.get(nid, []) if e["rel"] == "derived_from" and s.nodes[e["dst"]]["type"] == "result"
|
|
190
289
|
and not fallen(s, e["dst"])]
|
|
290
|
+
rs += [e["dst"] for e in s.out.get(nid, []) if e["rel"] == "cites" and s.nodes[e["dst"]]["type"] == "source"
|
|
291
|
+
and lifecycle(s, e["dst"]) == "active" and not fallen(s, e["dst"])]
|
|
191
292
|
rs = list(dict.fromkeys(rs))
|
|
192
293
|
if not rs:
|
|
193
294
|
return []
|
|
@@ -439,6 +540,15 @@ def affected_by(s: Snap, touched: set) -> set:
|
|
|
439
540
|
invs = {x for x in stack if s.nodes[x]["type"] == "investigation"}
|
|
440
541
|
if invs:
|
|
441
542
|
stack += [i for i, n in s.nodes.items() if n["inv"] in invs]
|
|
543
|
+
# a changed file reaches the results that name no file through no edge (see stale): under a workflow, every such
|
|
544
|
+
# result in the file's investigation may have gone stale
|
|
545
|
+
moved = {s.nodes[x]["inv"] for x in stack if s.nodes[x]["type"] == "artifact"}
|
|
546
|
+
if moved:
|
|
547
|
+
for i, n in s.nodes.items():
|
|
548
|
+
if n["type"] == "result" and n["inv"] in moved and workflow_profile(s, i):
|
|
549
|
+
run = producing_run(s, i)
|
|
550
|
+
if not (run and any(e["rel"] == "uses" for e in s.out.get(run, []))):
|
|
551
|
+
stack.append(i)
|
|
442
552
|
while stack:
|
|
443
553
|
y = stack.pop()
|
|
444
554
|
if y in out:
|
|
@@ -480,7 +590,7 @@ def state(s: Snap, nid: str) -> dict:
|
|
|
480
590
|
moved = basis_changed(s, nid)
|
|
481
591
|
if moved:
|
|
482
592
|
st["basis_changed"] = moved
|
|
483
|
-
if t in ("result", "claim"):
|
|
593
|
+
if t in ("result", "claim", "source", "decision"):
|
|
484
594
|
old = stale(s, nid)
|
|
485
595
|
if old:
|
|
486
596
|
st["stale"] = old
|
|
@@ -260,9 +260,10 @@ def report(g: "Graph", inv: str) -> dict:
|
|
|
260
260
|
st = D.state(s, c)
|
|
261
261
|
claims.append({**_brief_min(s, c), "lifecycle": st["lifecycle"], "review": (st.get("review") or {}).get("state"),
|
|
262
262
|
"verdicts": verdicts(c), "v": s.nodes[c]["v"], "evidence": st.get("evidence"),
|
|
263
|
-
"lines": st.get("independent_lines")})
|
|
263
|
+
"lines": st.get("independent_lines"), "stale": list(st.get("stale") or [])})
|
|
264
264
|
results = [{**_brief_min(s, r), "outcome": D.state(s, r).get("outcome"), "by": s.nodes[r]["created_by"],
|
|
265
|
-
"in_a_claim": r in cited_by_claim, "lifecycle": D.state(s, r)["lifecycle"]
|
|
265
|
+
"in_a_claim": r in cited_by_claim, "lifecycle": D.state(s, r)["lifecycle"],
|
|
266
|
+
"stale": list(D.state(s, r).get("stale") or [])} for r in by_type["result"]]
|
|
266
267
|
hyps = [{**_brief_min(s, h), "lifecycle": D.state(s, h)["lifecycle"],
|
|
267
268
|
"support": D.state(s, h).get("independent_lines"), "challenges": D.state(s, h).get("challenges")}
|
|
268
269
|
for h in by_type["hypothesis"]]
|
|
@@ -277,6 +278,8 @@ def report(g: "Graph", inv: str) -> dict:
|
|
|
277
278
|
open_items = {
|
|
278
279
|
"claims_unreviewed": [c["code"] for c in live if c["review"] in (None, "unchecked", "needs_revalidation")],
|
|
279
280
|
"claims_contested": [c["code"] for c in live if c["review"] == "contested"],
|
|
281
|
+
# a check made on evidence that has since gone stale (a file superseded, a page changed) is not a check now
|
|
282
|
+
"claims_stale": [c["code"] for c in live if c["stale"]],
|
|
280
283
|
"results_in_no_claim": [r["code"] for r in results if not r["in_a_claim"] and r["lifecycle"] != "withdrawn"
|
|
281
284
|
and r["by"] != g.policy.instrument],
|
|
282
285
|
"experiments_not_preregistered": [x["code"] for x in exps if not x["preregistered"]],
|
|
@@ -295,7 +298,7 @@ def report(g: "Graph", inv: str) -> dict:
|
|
|
295
298
|
prof = profiles.profile_of(s, inv)
|
|
296
299
|
if prof and prof.workflow:
|
|
297
300
|
stale = [{"code": s.nodes[i]["code"], "type": s.nodes[i]["type"], "why": D.state(s, i)["stale"]}
|
|
298
|
-
for i in by_type["result"] + by_type["claim"] if D.state(s, i).get("stale")
|
|
301
|
+
for i in by_type["result"] + by_type["claim"] + by_type["source"] if D.state(s, i).get("stale")
|
|
299
302
|
and D.state(s, i)["lifecycle"] != "withdrawn"]
|
|
300
303
|
out["workflow"] = {"profile": prof.id, "report": prof.workflow.report, "method": prof.workflow.method,
|
|
301
304
|
"questions": workflow.stages(s, inv, s.now), "stale": stale}
|
|
@@ -310,11 +313,13 @@ def report_markdown(r: dict) -> str:
|
|
|
310
313
|
"Interpretation goes below this section._", "", "### Claims", "",
|
|
311
314
|
"| claim | standing | review | verdicts | statement |", "|---|---|---|---|---|"]
|
|
312
315
|
for c in r["claims"]:
|
|
313
|
-
|
|
316
|
+
rv = (c['review'] or '-') + (" · STALE" if c.get("stale") else "")
|
|
317
|
+
L.append(f"| {c['code']} v{c['v']} | {c['lifecycle']} | {rv} | "
|
|
314
318
|
f"{'; '.join(c['verdicts']) or '-'} | {cut(c['title'])} |")
|
|
315
319
|
L += ["", "### Results", "", "| result | outcome | by | carried by a claim | summary |", "|---|---|---|---|---|"]
|
|
316
320
|
for x in r["results"]:
|
|
317
|
-
|
|
321
|
+
oc = (x['outcome'] or '-') + (" · STALE" if x.get("stale") else "")
|
|
322
|
+
L.append(f"| {x['code']} | {oc} | {x['by']} | {'yes' if x['in_a_claim'] else 'NO'} | "
|
|
318
323
|
f"{cut(x['title'])} |")
|
|
319
324
|
L += ["", "### Hypotheses", ""]
|
|
320
325
|
L += [f"- {h['code']} ({h['lifecycle']}; support lines {h['support']}, challenges {h['challenges']}): "
|
|
@@ -329,12 +334,14 @@ def report_markdown(r: dict) -> str:
|
|
|
329
334
|
for q in w["questions"]:
|
|
330
335
|
nxt = "complete" if q["complete"] else cut(q["next"], 140)
|
|
331
336
|
L.append(f"| {q['code']} | {q['stage']} ({q['index']}/{q['of']}) | {nxt} |")
|
|
332
|
-
L += ["", "Stale evidence (
|
|
337
|
+
L += ["", "Stale evidence (a file it used was superseded or withdrawn, its page changed, or valid_until passed): "
|
|
333
338
|
+ ("; ".join(f"{x['code']} — {x['why'][0]}" for x in w["stale"]) if w["stale"] else "none")]
|
|
334
339
|
o = r["open"]
|
|
335
340
|
L += ["", "### Open items", "",
|
|
336
341
|
f"- claims not yet reviewed: {', '.join(o['claims_unreviewed']) or 'none'}",
|
|
337
342
|
f"- claims contested: {', '.join(o['claims_contested']) or 'none'}",
|
|
343
|
+
f"- claims on stale evidence (what they rest on has changed since; a check of them no longer counts): "
|
|
344
|
+
f"{', '.join(o.get('claims_stale') or []) or 'none'}",
|
|
338
345
|
f"- results no claim carries (their conclusions are unreviewed): {', '.join(o['results_in_no_claim']) or 'none'}",
|
|
339
346
|
f"- experiments not preregistered: {', '.join(o['experiments_not_preregistered']) or 'none'}"]
|
|
340
347
|
sp = r["spend"]
|