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.
- {hypgraph-0.5.2 → hypgraph-0.5.3}/CHANGELOG.md +11 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/PKG-INFO +3 -2
- {hypgraph-0.5.2 → hypgraph-0.5.3}/README.md +2 -1
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/__init__.py +1 -1
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/derive.py +81 -1
- hypgraph-0.5.3/tests/test_unnamed_inputs.py +94 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/.gitignore +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/AGENTS.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/CITATION.cff +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/LICENSE +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/NOTICE +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/PLAN.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/decisions/ADR-002-governance-profiles.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/profile-workflows-plan.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/review-2026-10-24.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/spec.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/workflows/README.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/workflows/knowledge.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/workflows/optimization.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/workflows/scientific.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/workflows/software.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/docs/workflows/trading.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/pyproject.toml +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/__main__.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/bundle.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/checks.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/cli.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/coord.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/demos/__init__.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/demos/broad_street.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/graph.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/language.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/mcp.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/plan.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/policy.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/profiles.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/refusals.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/report_check.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/schema.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/server.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/skill/SKILL.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/stats.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/store.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/ui/__init__.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/ui/atlas.css +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/ui/atlas.js +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/ui/hypgraph.svg +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/ui/index.html +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/ui/vendor/cose-base.js +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/ui/vendor/layout-base.js +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/src/hypgraph/workflow.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/studies/harmony/tests/test_suite.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_cancel_releases_lease.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_coord.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_d10.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_engine.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_engine_fixes.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_incremental.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_log_and_check.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_order_detail.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_profiles.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_profiles_041.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_questions_not_assertions.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_refusals.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_report_answers.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_server_auth.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_stats_values.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_surfaces.py +0 -0
- {hypgraph-0.5.2 → hypgraph-0.5.3}/tests/test_vocabulary_friction.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|