hypgraph 0.5.2__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.2 → hypgraph-0.5.3}/CHANGELOG.md +11 -0
  2. {hypgraph-0.5.2 → hypgraph-0.5.3}/PKG-INFO +3 -2
  3. {hypgraph-0.5.2 → hypgraph-0.5.3}/README.md +2 -1
  4. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/__init__.py +1 -1
  5. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/derive.py +81 -1
  6. hypgraph-0.5.3/tests/test_unnamed_inputs.py +94 -0
  7. {hypgraph-0.5.2 → hypgraph-0.5.3}/.gitignore +0 -0
  8. {hypgraph-0.5.2 → hypgraph-0.5.3}/AGENTS.md +0 -0
  9. {hypgraph-0.5.2 → hypgraph-0.5.3}/CITATION.cff +0 -0
  10. {hypgraph-0.5.2 → hypgraph-0.5.3}/LICENSE +0 -0
  11. {hypgraph-0.5.2 → hypgraph-0.5.3}/NOTICE +0 -0
  12. {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/PLAN.md +0 -0
  13. {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
  14. {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/decisions/ADR-002-governance-profiles.md +0 -0
  15. {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/profile-workflows-plan.md +0 -0
  16. {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/review-2026-10-24.md +0 -0
  17. {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/spec.md +0 -0
  18. {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/workflows/README.md +0 -0
  19. {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/workflows/knowledge.md +0 -0
  20. {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/workflows/optimization.md +0 -0
  21. {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/workflows/scientific.md +0 -0
  22. {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/workflows/software.md +0 -0
  23. {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/workflows/trading.md +0 -0
  24. {hypgraph-0.5.2 → hypgraph-0.5.3}/pyproject.toml +0 -0
  25. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/__main__.py +0 -0
  26. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/bundle.py +0 -0
  27. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/checks.py +0 -0
  28. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/cli.py +0 -0
  29. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/coord.py +0 -0
  30. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/demos/__init__.py +0 -0
  31. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/demos/broad_street.py +0 -0
  32. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/graph.py +0 -0
  33. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/language.py +0 -0
  34. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/mcp.py +0 -0
  35. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/plan.py +0 -0
  36. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/policy.py +0 -0
  37. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/profiles.py +0 -0
  38. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/refusals.py +0 -0
  39. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/report_check.py +0 -0
  40. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/schema.py +0 -0
  41. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/server.py +0 -0
  42. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/skill/SKILL.md +0 -0
  43. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/stats.py +0 -0
  44. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/store.py +0 -0
  45. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/ui/__init__.py +0 -0
  46. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/ui/atlas.css +0 -0
  47. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/ui/atlas.js +0 -0
  48. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/ui/hypgraph.svg +0 -0
  49. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/ui/index.html +0 -0
  50. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
  51. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/ui/vendor/cose-base.js +0 -0
  52. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
  53. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
  54. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/ui/vendor/layout-base.js +0 -0
  55. {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/workflow.py +0 -0
  56. {hypgraph-0.5.2 → hypgraph-0.5.3}/studies/harmony/tests/test_suite.py +0 -0
  57. {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_cancel_releases_lease.py +0 -0
  58. {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_coord.py +0 -0
  59. {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_d10.py +0 -0
  60. {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_engine.py +0 -0
  61. {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_engine_fixes.py +0 -0
  62. {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_incremental.py +0 -0
  63. {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_log_and_check.py +0 -0
  64. {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_order_detail.py +0 -0
  65. {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_profiles.py +0 -0
  66. {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_profiles_041.py +0 -0
  67. {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_questions_not_assertions.py +0 -0
  68. {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_refusals.py +0 -0
  69. {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_report_answers.py +0 -0
  70. {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_server_auth.py +0 -0
  71. {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_stats_values.py +0 -0
  72. {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_surfaces.py +0 -0
  73. {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_vocabulary_friction.py +0 -0
  74. {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_workflows.py +0 -0
@@ -1,5 +1,16 @@
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
+
3
14
  ## 0.5.2 — 2026-10-26: freshness for sources, stale evidence in the report
4
15
 
5
16
  For P3 (the change suite and the instruments in HypSwarm), the record had to know when a *page* moved, not only a
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hypgraph
3
- Version: 0.5.2
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/
@@ -152,7 +152,8 @@ record behaves exactly as before.
152
152
  met, every gate MET / NOT MET with what would meet it); `GET /api/graph/stages`; MCP tool `stage`. A workflow
153
153
  widens what puts dependants AT RISK — `uses` (a corrected data file, a superseded diff or instance), `cites` (a
154
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
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
156
157
  verified failure each complete a question the gates alone never would.
157
158
 
158
159
  Profiles are versioned and frozen: a record written under `trading@1` means the same thing in a year, and the `@1`
@@ -120,7 +120,8 @@ record behaves exactly as before.
120
120
  met, every gate MET / NOT MET with what would meet it); `GET /api/graph/stages`; MCP tool `stage`. A workflow
121
121
  widens what puts dependants AT RISK — `uses` (a corrected data file, a superseded diff or instance), `cites` (a
122
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
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
124
125
  verified failure each complete a question the gates alone never would.
125
126
 
126
127
  Profiles are versioned and frozen: a record written under `trading@1` means the same thing in a year, and the `@1`
@@ -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.2"
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."""
@@ -169,10 +177,68 @@ def excerpts(s: Snap, sid: str) -> list[str]:
169
177
  and s.nodes[e["src"]]["type"] == "artifact" and (s.nodes[e["src"]]["body"].get("kind") or "") == "excerpt"]
170
178
 
171
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
+
172
236
  def stale(s: Snap, nid: str) -> list[str]:
173
237
  """Why a result (or a claim through its results) no longer speaks for what it measured; [] when fresh. A result
174
238
  is stale when the run that produced it used an artifact that is now superseded or withdrawn, or when its
175
- ``valid_until`` has passed. A source is stale when the host re-fetched its page and the page as it is now no
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
176
242
  longer contains its passage (the live excerpt says ``contains_passage: false``), or every excerpt of it was
177
243
  superseded. A claim is stale when it has results or cited sources and every one of them is stale."""
178
244
  n = s.nodes[nid]
@@ -200,6 +266,11 @@ def stale(s: Snap, nid: str) -> list[str]:
200
266
  a = s.nodes[e["dst"]]
201
267
  kind = a["body"].get("kind") or "artifact"
202
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")
203
274
  vu = str(n["body"].get("valid_until") or "").strip()
204
275
  if vu and s.now and s.now[:len(vu)] > vu:
205
276
  out.append(f"valid_until {vu} has passed")
@@ -469,6 +540,15 @@ def affected_by(s: Snap, touched: set) -> set:
469
540
  invs = {x for x in stack if s.nodes[x]["type"] == "investigation"}
470
541
  if invs:
471
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)
472
552
  while stack:
473
553
  y = stack.pop()
474
554
  if y in out:
@@ -0,0 +1,94 @@
1
+ """Under a workflow, a result whose run names no file it read (no ``uses``) can't show it read the new version of a
2
+ file that changed after it — so it goes stale then, like a result that named the old file. Results written after the
3
+ change, results that name their files, files of another question, run outputs, page excerpts and investigations with
4
+ no workflow are untouched."""
5
+ from hypgraph import Graph, Policy
6
+
7
+ H = "harness"
8
+ SHA_A, SHA_B, SHA_C = "a1" * 32, "b2" * 32, "c3" * 32
9
+
10
+
11
+ def _g(profile="scientific@2"):
12
+ g = Graph(policy=Policy(reviewers={"critic"}, extra_reserved={H}))
13
+ fields = {"title": "Does A beat B on the items?", "objective": "Count each model's correct items on the file."}
14
+ if profile:
15
+ fields["profile"] = profile
16
+ inv = g.create(author=H, type="investigation", fields=fields)["id"]
17
+ q1 = g.create(author=H, type="question", inv=inv, code="Q1", fields={"text": "Is A right more often than B?"})["id"]
18
+ return g, inv, q1
19
+
20
+
21
+ def _file(g, inv, sha, on=None, supersedes=None, kind=""):
22
+ fields = {"sha256": sha, "path": "items.csv"}
23
+ if kind:
24
+ fields["kind"] = kind
25
+ links = [{"rel": "part_of", "to": on}] if on else []
26
+ if supersedes:
27
+ links.append({"rel": "supersedes", "to": supersedes})
28
+ return g.create(author="vendor", type="artifact", inv=inv, fields=fields, links=links)["id"]
29
+
30
+
31
+ def _result(g, inv, q, uses=None):
32
+ run = g.create(author="analyst", type="run", inv=inv, fields={"kind": "agent", "name": "count", "state": "succeeded"},
33
+ links=[{"rel": "uses", "to": u, "data": {"role": "data"}} for u in uses or []])["id"]
34
+ rs = g.create(author="analyst", type="result", inv=inv, fields={"summary": "Counted the correct items per model.",
35
+ "values": {"a_correct": {"value": 827}}, "outcome": "positive"},
36
+ links=[{"rel": "produced", "from": run}])["id"]
37
+ c = g.create(author="analyst", type="claim", inv=inv, fields={"statement": "Model A is right more often than B.",
38
+ "scope": "The items file as the vendor sent it."},
39
+ links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs}])["id"]
40
+ return rs, c
41
+
42
+
43
+ def test_a_result_naming_no_file_goes_stale_when_a_file_changes_after_it():
44
+ g, inv, q = _g()
45
+ a1 = _file(g, inv, SHA_A)
46
+ rs, c = _result(g, inv, q)
47
+ assert not g.state(rs).get("stale")
48
+ a2 = _file(g, inv, SHA_B, supersedes=a1)
49
+ why = g.state(rs)["stale"]
50
+ assert why and "names no file it read" in why[0] and g.get(a1)["code"] in why[0]
51
+ assert g.state(c).get("stale") # the claim rests on it, so it is stale too
52
+ # a result written after the change could have read the new file: fresh
53
+ rs2, c2 = _result(g, inv, q)
54
+ assert not g.state(rs2).get("stale") and not g.state(c2).get("stale")
55
+ # withdrawing a file counts the same as replacing it
56
+ g2, inv2, p = _g()
57
+ b1 = _file(g2, inv2, SHA_A)
58
+ rs3, _ = _result(g2, inv2, p)
59
+ g2.withdraw(author="vendor", node=b1, why="the vendor pulled the file")
60
+ assert g2.state(rs3).get("stale")
61
+ assert a2
62
+
63
+
64
+ def test_a_result_that_names_its_files_follows_the_usual_rule():
65
+ g, inv, q = _g()
66
+ a1 = _file(g, inv, SHA_A)
67
+ other = g.create(author="vendor", type="artifact", inv=inv, fields={"sha256": SHA_C, "path": "labels.csv"})["id"]
68
+ rs, _ = _result(g, inv, q, uses=[a1])
69
+ _file(g, inv, SHA_B, supersedes=other) # a file it didn't use changed: still fresh
70
+ assert not g.state(rs).get("stale")
71
+ _file(g, inv, "d4" * 32, supersedes=a1) # the file it used changed: stale, the usual way
72
+ assert "is superseded" in g.state(rs)["stale"][0]
73
+
74
+
75
+ def test_files_of_another_question_run_outputs_and_excerpts_do_not_count():
76
+ g, inv, q1 = _g()
77
+ q2 = g.create(author=H, type="question", inv=inv, code="Q2", fields={"text": "How many items is B right on?"})["id"]
78
+ f2 = _file(g, inv, SHA_A, on=q2)
79
+ out = g.create(author="analyst", type="artifact", inv=inv, fields={"sha256": SHA_C, "path": "plot.png"})["id"]
80
+ run = g.create(author="analyst", type="run", inv=inv, fields={"kind": "agent", "name": "plot", "state": "succeeded"},
81
+ links=[{"rel": "produced", "to": out}])["id"]
82
+ rs, _ = _result(g, inv, q1)
83
+ _file(g, inv, SHA_B, on=q2, supersedes=f2) # Q2's file: not Q1's result's business
84
+ g.create(author="analyst", type="artifact", inv=inv, fields={"sha256": "e5" * 32, "path": "plot.png"},
85
+ links=[{"rel": "supersedes", "to": out}]) # a run's output replaced: not an input
86
+ assert run and not g.state(rs).get("stale")
87
+
88
+
89
+ def test_no_workflow_no_rule():
90
+ g, inv, q = _g(profile="")
91
+ a1 = _file(g, inv, SHA_A)
92
+ rs, _ = _result(g, inv, q)
93
+ _file(g, inv, SHA_B, supersedes=a1)
94
+ assert not g.state(rs).get("stale")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes