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.
Files changed (74) hide show
  1. {hypgraph-0.5.1 → hypgraph-0.5.3}/CHANGELOG.md +42 -0
  2. {hypgraph-0.5.1 → hypgraph-0.5.3}/PKG-INFO +24 -14
  3. {hypgraph-0.5.1 → hypgraph-0.5.3}/README.md +23 -13
  4. {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/profile-workflows-plan.md +1 -1
  5. {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/spec.md +6 -1
  6. {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/workflows/knowledge.md +5 -1
  7. {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/workflows/optimization.md +4 -1
  8. {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/workflows/software.md +6 -0
  9. {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/workflows/trading.md +4 -2
  10. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/__init__.py +1 -1
  11. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/derive.py +112 -2
  12. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/plan.py +13 -6
  13. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/report_check.py +276 -2
  14. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/workflow.py +52 -14
  15. hypgraph-0.5.3/tests/test_report_answers.py +184 -0
  16. hypgraph-0.5.3/tests/test_unnamed_inputs.py +94 -0
  17. {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_workflows.py +98 -0
  18. {hypgraph-0.5.1 → hypgraph-0.5.3}/.gitignore +0 -0
  19. {hypgraph-0.5.1 → hypgraph-0.5.3}/AGENTS.md +0 -0
  20. {hypgraph-0.5.1 → hypgraph-0.5.3}/CITATION.cff +0 -0
  21. {hypgraph-0.5.1 → hypgraph-0.5.3}/LICENSE +0 -0
  22. {hypgraph-0.5.1 → hypgraph-0.5.3}/NOTICE +0 -0
  23. {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/PLAN.md +0 -0
  24. {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
  25. {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/decisions/ADR-002-governance-profiles.md +0 -0
  26. {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/review-2026-10-24.md +0 -0
  27. {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/workflows/README.md +0 -0
  28. {hypgraph-0.5.1 → hypgraph-0.5.3}/docs/workflows/scientific.md +0 -0
  29. {hypgraph-0.5.1 → hypgraph-0.5.3}/pyproject.toml +0 -0
  30. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/__main__.py +0 -0
  31. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/bundle.py +0 -0
  32. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/checks.py +0 -0
  33. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/cli.py +0 -0
  34. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/coord.py +0 -0
  35. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/demos/__init__.py +0 -0
  36. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/demos/broad_street.py +0 -0
  37. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/graph.py +0 -0
  38. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/language.py +0 -0
  39. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/mcp.py +0 -0
  40. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/policy.py +0 -0
  41. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/profiles.py +0 -0
  42. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/refusals.py +0 -0
  43. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/schema.py +0 -0
  44. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/server.py +0 -0
  45. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/skill/SKILL.md +0 -0
  46. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/stats.py +0 -0
  47. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/store.py +0 -0
  48. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/ui/__init__.py +0 -0
  49. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/ui/atlas.css +0 -0
  50. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/ui/atlas.js +0 -0
  51. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/ui/hypgraph.svg +0 -0
  52. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/ui/index.html +0 -0
  53. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
  54. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/ui/vendor/cose-base.js +0 -0
  55. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
  56. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
  57. {hypgraph-0.5.1 → hypgraph-0.5.3}/src/hypgraph/ui/vendor/layout-base.js +0 -0
  58. {hypgraph-0.5.1 → hypgraph-0.5.3}/studies/harmony/tests/test_suite.py +0 -0
  59. {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_cancel_releases_lease.py +0 -0
  60. {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_coord.py +0 -0
  61. {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_d10.py +0 -0
  62. {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_engine.py +0 -0
  63. {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_engine_fixes.py +0 -0
  64. {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_incremental.py +0 -0
  65. {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_log_and_check.py +0 -0
  66. {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_order_detail.py +0 -0
  67. {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_profiles.py +0 -0
  68. {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_profiles_041.py +0 -0
  69. {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_questions_not_assertions.py +0 -0
  70. {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_refusals.py +0 -0
  71. {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_server_auth.py +0 -0
  72. {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_stats_values.py +0 -0
  73. {hypgraph-0.5.1 → hypgraph-0.5.3}/tests/test_surfaces.py +0 -0
  74. {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.1
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 the record enforces it, so it never depends on a prompt being followed. Pin one on the
139
- investigation (`"profile": "software@1"`); without one, the record behaves exactly as before.
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 | an order is done when… | the record refuses | the investigation is done when |
143
+ | profile | the stages a question passes through | the record refuses | done when |
142
144
  |---|---|---|---|
143
- | `scientific@1` | a checked claim answers the question | numbers no result accounts for, "never" without the count, "significant" without the test | every question is settled |
144
- | `software@1` | **the instrument ran the tests** (`tests_pass`) and a claim states it | *"it passes"* without an instrument-recorded run | every requirement's claim rests on a passing run |
145
- | `trading@1` | a backtest is **out of sample and after costs** (`backtested_oos`) | a backtest without the costs it charged; *"edge"* without both | a sized paper position is a decision based on the claim |
146
- | `knowledge@1` | the claim **cites a source node** the record holds (`sourced`) | a factual claim with no source | every claim cites a source |
147
- | `optimization@1` | a result carries **objective, feasible, and a bound or gap** (`feasible_bounded`) | an objective without feasibility; *"optimal"* without a bound | a feasible incumbent with a bound exists |
148
-
149
- Profiles are versioned and frozen: a record written under `trading@1` means the same thing in a year, and a changed
150
- rule is `@2`. `completion(inv)` reports each criterion; the briefing names the profile and where it stands;
151
- `hypgraph serve` exposes `/api/graph/completion` and `/api/graph/profiles`. Adding a discipline is one `Profile` —
152
- vocabulary, default predicates, domain predicates, refusals, completion — and the rest of the record is unchanged.
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 the record enforces it, so it never depends on a prompt being followed. Pin one on the
107
- investigation (`"profile": "software@1"`); without one, the record behaves exactly as before.
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 | an order is done when… | the record refuses | the investigation is done when |
111
+ | profile | the stages a question passes through | the record refuses | done when |
110
112
  |---|---|---|---|
111
- | `scientific@1` | a checked claim answers the question | numbers no result accounts for, "never" without the count, "significant" without the test | every question is settled |
112
- | `software@1` | **the instrument ran the tests** (`tests_pass`) and a claim states it | *"it passes"* without an instrument-recorded run | every requirement's claim rests on a passing run |
113
- | `trading@1` | a backtest is **out of sample and after costs** (`backtested_oos`) | a backtest without the costs it charged; *"edge"* without both | a sized paper position is a decision based on the claim |
114
- | `knowledge@1` | the claim **cites a source node** the record holds (`sourced`) | a factual claim with no source | every claim cites a source |
115
- | `optimization@1` | a result carries **objective, feasible, and a bound or gap** (`feasible_bounded`) | an objective without feasibility; *"optimal"* without a bound | a feasible incumbent with a bound exists |
116
-
117
- Profiles are versioned and frozen: a record written under `trading@1` means the same thing in a year, and a changed
118
- rule is `@2`. `completion(inv)` reports each criterion; the briefing names the profile and where it stands;
119
- `hypgraph serve` exposes `/api/graph/completion` and `/api/graph/profiles`. Adding a discipline is one `Profile` —
120
- vocabulary, default predicates, domain predicates, refusals, completion — and the rest of the record is unchanged.
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 is next.
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. Quoted passages and negated words ("cannot be proven") are not treated as
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**, with `NUMBER_NOT_IN_PASSAGE`; both need the host's fetch at review time.
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** (`check_solution` reproducer). Built now: `result_reviewed` —
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 now, checked later**: `backtested` and `EDGE_WITHOUT_TRIALS`
74
- need the value; the instrument's count of the parameter grid is P3.
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.1"
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. A claim is stale when it has results and every one of them is stale."""
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"]} for r in by_type["result"]]
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
- L.append(f"| {c['code']} v{c['v']} | {c['lifecycle']} | {c['review'] or '-'} | "
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
- L.append(f"| {x['code']} | {x['outcome'] or '-'} | {x['by']} | {'yes' if x['in_a_claim'] else 'NO'} | "
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 (the run used an artifact since superseded or withdrawn, or valid_until passed): "
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"]