hypgraph 0.5.1__tar.gz → 0.5.2__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 (73) hide show
  1. {hypgraph-0.5.1 → hypgraph-0.5.2}/CHANGELOG.md +31 -0
  2. {hypgraph-0.5.1 → hypgraph-0.5.2}/PKG-INFO +23 -14
  3. {hypgraph-0.5.1 → hypgraph-0.5.2}/README.md +22 -13
  4. {hypgraph-0.5.1 → hypgraph-0.5.2}/docs/profile-workflows-plan.md +1 -1
  5. {hypgraph-0.5.1 → hypgraph-0.5.2}/docs/spec.md +6 -1
  6. {hypgraph-0.5.1 → hypgraph-0.5.2}/docs/workflows/knowledge.md +5 -1
  7. {hypgraph-0.5.1 → hypgraph-0.5.2}/docs/workflows/optimization.md +4 -1
  8. {hypgraph-0.5.1 → hypgraph-0.5.2}/docs/workflows/software.md +6 -0
  9. {hypgraph-0.5.1 → hypgraph-0.5.2}/docs/workflows/trading.md +4 -2
  10. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/__init__.py +1 -1
  11. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/derive.py +32 -2
  12. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/plan.py +13 -6
  13. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/report_check.py +276 -2
  14. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/workflow.py +52 -14
  15. hypgraph-0.5.2/tests/test_report_answers.py +184 -0
  16. {hypgraph-0.5.1 → hypgraph-0.5.2}/tests/test_workflows.py +98 -0
  17. {hypgraph-0.5.1 → hypgraph-0.5.2}/.gitignore +0 -0
  18. {hypgraph-0.5.1 → hypgraph-0.5.2}/AGENTS.md +0 -0
  19. {hypgraph-0.5.1 → hypgraph-0.5.2}/CITATION.cff +0 -0
  20. {hypgraph-0.5.1 → hypgraph-0.5.2}/LICENSE +0 -0
  21. {hypgraph-0.5.1 → hypgraph-0.5.2}/NOTICE +0 -0
  22. {hypgraph-0.5.1 → hypgraph-0.5.2}/docs/PLAN.md +0 -0
  23. {hypgraph-0.5.1 → hypgraph-0.5.2}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
  24. {hypgraph-0.5.1 → hypgraph-0.5.2}/docs/decisions/ADR-002-governance-profiles.md +0 -0
  25. {hypgraph-0.5.1 → hypgraph-0.5.2}/docs/review-2026-10-24.md +0 -0
  26. {hypgraph-0.5.1 → hypgraph-0.5.2}/docs/workflows/README.md +0 -0
  27. {hypgraph-0.5.1 → hypgraph-0.5.2}/docs/workflows/scientific.md +0 -0
  28. {hypgraph-0.5.1 → hypgraph-0.5.2}/pyproject.toml +0 -0
  29. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/__main__.py +0 -0
  30. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/bundle.py +0 -0
  31. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/checks.py +0 -0
  32. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/cli.py +0 -0
  33. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/coord.py +0 -0
  34. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/demos/__init__.py +0 -0
  35. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/demos/broad_street.py +0 -0
  36. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/graph.py +0 -0
  37. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/language.py +0 -0
  38. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/mcp.py +0 -0
  39. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/policy.py +0 -0
  40. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/profiles.py +0 -0
  41. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/refusals.py +0 -0
  42. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/schema.py +0 -0
  43. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/server.py +0 -0
  44. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/skill/SKILL.md +0 -0
  45. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/stats.py +0 -0
  46. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/store.py +0 -0
  47. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/ui/__init__.py +0 -0
  48. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/ui/atlas.css +0 -0
  49. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/ui/atlas.js +0 -0
  50. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/ui/hypgraph.svg +0 -0
  51. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/ui/index.html +0 -0
  52. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
  53. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/ui/vendor/cose-base.js +0 -0
  54. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
  55. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
  56. {hypgraph-0.5.1 → hypgraph-0.5.2}/src/hypgraph/ui/vendor/layout-base.js +0 -0
  57. {hypgraph-0.5.1 → hypgraph-0.5.2}/studies/harmony/tests/test_suite.py +0 -0
  58. {hypgraph-0.5.1 → hypgraph-0.5.2}/tests/test_cancel_releases_lease.py +0 -0
  59. {hypgraph-0.5.1 → hypgraph-0.5.2}/tests/test_coord.py +0 -0
  60. {hypgraph-0.5.1 → hypgraph-0.5.2}/tests/test_d10.py +0 -0
  61. {hypgraph-0.5.1 → hypgraph-0.5.2}/tests/test_engine.py +0 -0
  62. {hypgraph-0.5.1 → hypgraph-0.5.2}/tests/test_engine_fixes.py +0 -0
  63. {hypgraph-0.5.1 → hypgraph-0.5.2}/tests/test_incremental.py +0 -0
  64. {hypgraph-0.5.1 → hypgraph-0.5.2}/tests/test_log_and_check.py +0 -0
  65. {hypgraph-0.5.1 → hypgraph-0.5.2}/tests/test_order_detail.py +0 -0
  66. {hypgraph-0.5.1 → hypgraph-0.5.2}/tests/test_profiles.py +0 -0
  67. {hypgraph-0.5.1 → hypgraph-0.5.2}/tests/test_profiles_041.py +0 -0
  68. {hypgraph-0.5.1 → hypgraph-0.5.2}/tests/test_questions_not_assertions.py +0 -0
  69. {hypgraph-0.5.1 → hypgraph-0.5.2}/tests/test_refusals.py +0 -0
  70. {hypgraph-0.5.1 → hypgraph-0.5.2}/tests/test_server_auth.py +0 -0
  71. {hypgraph-0.5.1 → hypgraph-0.5.2}/tests/test_stats_values.py +0 -0
  72. {hypgraph-0.5.1 → hypgraph-0.5.2}/tests/test_surfaces.py +0 -0
  73. {hypgraph-0.5.1 → hypgraph-0.5.2}/tests/test_vocabulary_friction.py +0 -0
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.2 — 2026-10-26: freshness for sources, stale evidence in the report
4
+
5
+ For P3 (the change suite and the instruments in HypSwarm), the record had to know when a *page* moved, not only a
6
+ file:
7
+ - **A source is fresh while its page still says it.** A host that re-fetches a source's page registers an
8
+ `excerpt` artifact (`derived_from` the source; `meta.contains_passage`, `meta.fetched`). The source is STALE when
9
+ every excerpt of it was superseded, or the live one says the page no longer contains its passage — an edit
10
+ elsewhere on the page keeps it fresh. A source never re-fetched is not stale: nothing says it moved.
11
+ - **A claim is stale when every result and every cited source behind it is.** The knowledge gates
12
+ (`source_quoted`, `sourced`) take only fresh sources, so a changed page reopens the question at *sourced*.
13
+ - **The report marks it.** Claims and results on stale evidence read `checked · STALE` in the facts tables, a new
14
+ open item lists them, the workflow section lists stale sources too, and the report check treats calling a stale
15
+ claim settled as `REPORT_OVERSTATES_STANDING`.
16
+ - **Gates count only what still speaks for the question.** `result_exists`, `backtested` and `solved` take only
17
+ fresh results (their message says which went stale and why); `result_reviewed` doesn't count a check of a stale
18
+ result; a decision is stale when everything it was based on is, and `positioned` / `decided` take only fresh ones
19
+ — a position taken on a backtest the vendor later corrected has to be decided again.
20
+ - **The report check reads the answers block.** A fenced ```answers block (the part of a report a harness scores and a
21
+ reader copies) was skipped like any code block, so a report could carry an answer only stale evidence gave and pass.
22
+ Now each answer has to be one a current claim on its question makes — live, not stale — at the precision written and
23
+ with its sign: `REPORT_ANSWER_STALE` when only old claims or stale results give it (or a current claim binds the
24
+ same field to another value), `REPORT_ANSWER_UNBACKED` when no claim does or the question has no claim,
25
+ `REPORT_ANSWERS_UNREADABLE` for invalid JSON. Results count through the claims that bind them, and a count k of n
26
+ backs its rate; a number a claim names only as a superseded figure doesn't back an answer; URLs, paths and hashes
27
+ are provenance. Keys are question codes in any case, or a flat object checked against the investigation. Found by
28
+ HypSwarm's change suite.
29
+ - **The report check skips the record's own facts.** A line inside a "Facts from the research graph" block is not
30
+ linted when it is exactly a line the record generates now (a hypothesis that says "significantly" is a conjecture
31
+ the record lists, not the author's assertion). Anything else written under that heading is checked as usual.
32
+ Found when a report was refused for the hypothesis line the record itself had written.
33
+
3
34
  ## 0.5.1 — 2026-10-26
4
35
 
5
36
  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.2
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,30 @@ 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. An honest "it can't be answered" (a checked `inconclusive` claim), a declined thesis (`no_position`) and a
156
+ verified failure each complete a question the gates alone never would.
157
+
158
+ Profiles are versioned and frozen: a record written under `trading@1` means the same thing in a year, and the `@1`
159
+ profiles stay exactly as they were. `completion(inv)` reports each criterion; `hypgraph serve` exposes
160
+ `/api/graph/completion` and `/api/graph/profiles`. The method each workflow is taken from is quoted, one page each,
161
+ in [docs/workflows/](docs/workflows/). Adding a discipline is one `Profile` with a `Workflow`.
153
162
 
154
163
  ## Connect your agent
155
164
 
@@ -103,21 +103,30 @@ 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. An honest "it can't be answered" (a checked `inconclusive` claim), a declined thesis (`no_position`) and a
124
+ verified failure each complete a question the gates alone never would.
125
+
126
+ Profiles are versioned and frozen: a record written under `trading@1` means the same thing in a year, and the `@1`
127
+ profiles stay exactly as they were. `completion(inv)` reports each criterion; `hypgraph serve` exposes
128
+ `/api/graph/completion` and `/api/graph/profiles`. The method each workflow is taken from is quoted, one page each,
129
+ in [docs/workflows/](docs/workflows/). Adding a discipline is one `Profile` with a `Workflow`.
121
130
 
122
131
  ## Connect your agent
123
132
 
@@ -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.2"
20
20
 
21
21
  from .bundle import verify_bundle # noqa: E402
22
22
  from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
@@ -163,11 +163,32 @@ def downstream(s: Snap, x: str) -> list[tuple[str, str]]:
163
163
  return downs
164
164
 
165
165
 
166
+ def excerpts(s: Snap, sid: str) -> list[str]:
167
+ """The pages a host fetched for a source: artifacts ``kind=excerpt`` derived_from it (oldest first)."""
168
+ return [e["src"] for e in s.inn.get(sid, []) if e["rel"] == "derived_from"
169
+ and s.nodes[e["src"]]["type"] == "artifact" and (s.nodes[e["src"]]["body"].get("kind") or "") == "excerpt"]
170
+
171
+
166
172
  def stale(s: Snap, nid: str) -> list[str]:
167
173
  """Why a result (or a claim through its results) no longer speaks for what it measured; [] when fresh. A result
168
174
  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."""
175
+ ``valid_until`` has passed. A source is stale when the host re-fetched its page and the page as it is now no
176
+ longer contains its passage (the live excerpt says ``contains_passage: false``), or every excerpt of it was
177
+ superseded. A claim is stale when it has results or cited sources and every one of them is stale."""
170
178
  n = s.nodes[nid]
179
+ if n["type"] == "source":
180
+ ex = excerpts(s, nid)
181
+ if not ex:
182
+ return [] # never re-fetched: nothing says the page moved
183
+ live = [a for a in ex if lifecycle(s, a) == "active"]
184
+ if not live:
185
+ return [f"its page changed since it was read ({', '.join(s.nodes[a]['code'] or a for a in ex[-2:])} "
186
+ "superseded)"]
187
+ meta = [s.nodes[a]["body"].get("meta") or {} for a in live]
188
+ if all(m.get("contains_passage") is False for m in meta):
189
+ when = max((str(m.get("fetched") or "") for m in meta), default="")
190
+ return [f"the page no longer contains its passage (re-fetched{' ' + when if when else ''})"]
191
+ return []
171
192
  if n["type"] == "result":
172
193
  out = []
173
194
  run = producing_run(s, nid)
@@ -183,11 +204,20 @@ def stale(s: Snap, nid: str) -> list[str]:
183
204
  if vu and s.now and s.now[:len(vu)] > vu:
184
205
  out.append(f"valid_until {vu} has passed")
185
206
  return out
207
+ if n["type"] == "decision":
208
+ # a decision is stale when everything it was based on is (a position taken on a backtest the vendor corrected)
209
+ basis = [e["dst"] for e in s.out.get(nid, []) if e["rel"] == "based_on" and not fallen(s, e["dst"])]
210
+ reasons = [stale(s, b) for b in basis]
211
+ if basis and all(reasons):
212
+ return list(dict.fromkeys(x for r in reasons for x in r))
213
+ return []
186
214
  if n["type"] == "claim":
187
215
  rs = [e["src"] for e in s.inn.get(nid, []) if e["rel"] == "supports" and s.nodes[e["src"]]["type"] == "result"
188
216
  and not fallen(s, e["src"])]
189
217
  rs += [e["dst"] for e in s.out.get(nid, []) if e["rel"] == "derived_from" and s.nodes[e["dst"]]["type"] == "result"
190
218
  and not fallen(s, e["dst"])]
219
+ rs += [e["dst"] for e in s.out.get(nid, []) if e["rel"] == "cites" and s.nodes[e["dst"]]["type"] == "source"
220
+ and lifecycle(s, e["dst"]) == "active" and not fallen(s, e["dst"])]
191
221
  rs = list(dict.fromkeys(rs))
192
222
  if not rs:
193
223
  return []
@@ -480,7 +510,7 @@ def state(s: Snap, nid: str) -> dict:
480
510
  moved = basis_changed(s, nid)
481
511
  if moved:
482
512
  st["basis_changed"] = moved
483
- if t in ("result", "claim"):
513
+ if t in ("result", "claim", "source", "decision"):
484
514
  old = stale(s, nid)
485
515
  if old:
486
516
  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"]