hypgraph 0.1.0__py3-none-any.whl
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/__init__.py +28 -0
- hypgraph/__main__.py +3 -0
- hypgraph/bundle.py +216 -0
- hypgraph/checks.py +140 -0
- hypgraph/cli.py +445 -0
- hypgraph/demos/__init__.py +3 -0
- hypgraph/demos/broad_street.py +283 -0
- hypgraph/derive.py +390 -0
- hypgraph/graph.py +1002 -0
- hypgraph/mcp.py +461 -0
- hypgraph/plan.py +328 -0
- hypgraph/policy.py +51 -0
- hypgraph/schema.py +141 -0
- hypgraph/server.py +202 -0
- hypgraph/skill/SKILL.md +94 -0
- hypgraph/store.py +139 -0
- hypgraph/ui/__init__.py +7 -0
- hypgraph/ui/atlas.css +219 -0
- hypgraph/ui/atlas.js +1052 -0
- hypgraph/ui/hypgraph.svg +10 -0
- hypgraph/ui/index.html +30 -0
- hypgraph/ui/vendor/LICENSES.md +10 -0
- hypgraph/ui/vendor/cose-base.js +3214 -0
- hypgraph/ui/vendor/cytoscape-fcose.js +1549 -0
- hypgraph/ui/vendor/cytoscape.min.js +32 -0
- hypgraph/ui/vendor/layout-base.js +5230 -0
- hypgraph-0.1.0.dist-info/METADATA +140 -0
- hypgraph-0.1.0.dist-info/RECORD +31 -0
- hypgraph-0.1.0.dist-info/WHEEL +4 -0
- hypgraph-0.1.0.dist-info/entry_points.txt +2 -0
- hypgraph-0.1.0.dist-info/licenses/LICENSE +201 -0
hypgraph/derive.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Standing, derived on every read. Nothing here is stored or settable:
|
|
3
|
+
|
|
4
|
+
lifecycle active | superseded | withdrawn (withdrawals, supersede / correct edges)
|
|
5
|
+
review unchecked | checked | contested | needs_revalidation (reviews pinned to versions; reviewers only)
|
|
6
|
+
execution for runs: queued | running | succeeded | failed | … (as recorded)
|
|
7
|
+
outcome for results: positive | negative | inconclusive | expected_failure | skipped | failed
|
|
8
|
+
evidence for claims and hypotheses, the strongest live support: external > new_data > separate_implementation
|
|
9
|
+
> project_rerun > lab_recorded > agent_reported > unverified
|
|
10
|
+
lines independent lines of support (lines sharing a run, a data artifact or a source count once)
|
|
11
|
+
at_risk the fallen nodes it rests on; ``unsupported`` when every support it had has fallen
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections import defaultdict
|
|
17
|
+
from typing import Any, Optional
|
|
18
|
+
|
|
19
|
+
from .policy import Policy
|
|
20
|
+
from .schema import NEGATIVE, POSITIVE, PROFILE_LABEL, PROFILE_RANK, RISKY, SAME_TYPE
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Snap:
|
|
24
|
+
"""The whole graph at one event, in memory. Built by Graph.snapshot(); treat it as read-only."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, seq: int, policy: Policy):
|
|
27
|
+
self.seq = seq
|
|
28
|
+
self.policy = policy
|
|
29
|
+
self.nodes: dict[str, dict] = {}
|
|
30
|
+
self.edges: dict[int, dict] = {}
|
|
31
|
+
self.out: dict[str, list] = defaultdict(list)
|
|
32
|
+
self.inn: dict[str, list] = defaultdict(list)
|
|
33
|
+
self.withdrawn: dict[str, dict] = {}
|
|
34
|
+
self.leases: dict[str, dict] = {}
|
|
35
|
+
self._state: dict[str, dict] = {}
|
|
36
|
+
self.sig: Any = None
|
|
37
|
+
self.versions: dict[str, list] = defaultdict(list)
|
|
38
|
+
self.events: list[dict] = []
|
|
39
|
+
# highest rowid / id loaded per table: every table is append-only, so a later read loads only what's newer
|
|
40
|
+
self.marks: dict[str, int] = {"nodes": 0, "versions": 0, "edges": 0, "withdrawals": 0, "leases": 0}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def board(s: Snap, nid: str) -> set:
|
|
44
|
+
"""Whose verdicts count on this node: the policy's reviewers, the investigation's own, and the curators."""
|
|
45
|
+
inv = s.nodes.get(s.nodes[nid]["inv"]) if nid in s.nodes else None
|
|
46
|
+
extra = set((inv or {}).get("body", {}).get("reviewers") or [])
|
|
47
|
+
return set(s.policy.reviewers) | extra | set(s.policy.curators)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def lifecycle(s: Snap, nid: str, _seen: frozenset = frozenset()) -> str:
|
|
51
|
+
if nid in s.withdrawn:
|
|
52
|
+
return "withdrawn"
|
|
53
|
+
for e in s.inn.get(nid, []):
|
|
54
|
+
# superseded by a live replacement (a cycle of supersedes edges can't recurse forever)
|
|
55
|
+
if e["rel"] in SAME_TYPE and e["src"] not in _seen and lifecycle(s, e["src"], _seen | {nid}) == "active":
|
|
56
|
+
return "superseded"
|
|
57
|
+
n = s.nodes[nid]
|
|
58
|
+
if n["type"] == "investigation":
|
|
59
|
+
return n["body"].get("state") or "active"
|
|
60
|
+
return "active"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def review_state(s: Snap, nid: str) -> dict:
|
|
64
|
+
"""Reviews by board members, pinned to versions. An objection stays open until its reviewer (or a curator, or one
|
|
65
|
+
of the investigation's own reviewers) records ``resolved``; a negative verdict on the current version contests."""
|
|
66
|
+
n = s.nodes[nid]
|
|
67
|
+
brd = board(s, nid)
|
|
68
|
+
inv = s.nodes.get(n["inv"]) or {}
|
|
69
|
+
leads = set((inv.get("body") or {}).get("reviewers") or []) | set(s.policy.curators)
|
|
70
|
+
rows = []
|
|
71
|
+
for e in s.inn.get(nid, []):
|
|
72
|
+
if e["rel"] != "reviews":
|
|
73
|
+
continue
|
|
74
|
+
r = s.nodes.get(e["src"])
|
|
75
|
+
if not r or r["id"] in s.withdrawn:
|
|
76
|
+
continue
|
|
77
|
+
rows.append({"review": r["id"], "by": r["created_by"], "verdict": r["body"].get("verdict"),
|
|
78
|
+
"on_v": e["dst_v"] or 0, "at": r["created_at"],
|
|
79
|
+
"official": bool(r["body"].get("official", r["created_by"] in brd))})
|
|
80
|
+
rows.sort(key=lambda x: x["at"])
|
|
81
|
+
official = [x for x in rows if x["official"] and x["verdict"] != "comment"]
|
|
82
|
+
open_obj: dict[str, dict] = {}
|
|
83
|
+
for x in official:
|
|
84
|
+
if x["verdict"] == "objection":
|
|
85
|
+
open_obj[x["by"]] = x
|
|
86
|
+
elif x["verdict"] == "resolved":
|
|
87
|
+
if x["by"] in leads: # curators and the investigation's lead reviewers settle any dispute;
|
|
88
|
+
open_obj.clear() # everyone else, their own
|
|
89
|
+
else:
|
|
90
|
+
open_obj.pop(x["by"], None)
|
|
91
|
+
cur = [x for x in official if x["on_v"] == n["v"]]
|
|
92
|
+
latest: dict[str, dict] = {}
|
|
93
|
+
for x in cur:
|
|
94
|
+
if x["verdict"] not in ("objection", "resolved"):
|
|
95
|
+
latest[x["by"]] = x
|
|
96
|
+
positive = sorted({f"{x['by']}:{x['verdict']}" for x in latest.values() if x["verdict"] in POSITIVE})
|
|
97
|
+
negative = sorted({f"{x['by']}:{x['verdict']}" for x in latest.values() if x["verdict"] in NEGATIVE})
|
|
98
|
+
if open_obj or negative:
|
|
99
|
+
st = "contested"
|
|
100
|
+
elif positive:
|
|
101
|
+
st = "checked"
|
|
102
|
+
elif any(x["verdict"] in POSITIVE for x in official if x["on_v"] < n["v"]):
|
|
103
|
+
st = "needs_revalidation"
|
|
104
|
+
else:
|
|
105
|
+
st = "unchecked"
|
|
106
|
+
return {"state": st, "positive": positive, "negative": negative,
|
|
107
|
+
"open_objections": [{"by": x["by"], "review": x["review"]} for x in open_obj.values()],
|
|
108
|
+
"reviews": len(rows), "comments": sum(1 for x in rows if not x["official"] or x["verdict"] == "comment")}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def fallen(s: Snap, nid: str) -> bool:
|
|
112
|
+
"""Broken as a premise: withdrawn, contested, or a failed run / result. A superseded node is NOT fallen — it was
|
|
113
|
+
refined, not refuted — so what rests on it is flagged softly (rests_on_superseded), not put AT RISK."""
|
|
114
|
+
n = s.nodes[nid]
|
|
115
|
+
if lifecycle(s, nid) == "withdrawn":
|
|
116
|
+
return True
|
|
117
|
+
if n["type"] in ("claim", "result", "hypothesis") and review_state(s, nid)["state"] == "contested":
|
|
118
|
+
return True
|
|
119
|
+
if n["type"] == "run" and n["body"].get("state") in ("failed", "timeout", "cancelled", "interrupted"):
|
|
120
|
+
return True
|
|
121
|
+
if n["type"] == "result" and n["body"].get("outcome") == "failed":
|
|
122
|
+
return True
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def upstream(s: Snap, x: str) -> list[tuple[str, str]]:
|
|
127
|
+
"""What x rests on, one step: premises (requires), derivations, a decision's basis, and the run that produced it.
|
|
128
|
+
Support is counted separately (unsupported): one fallen support among several doesn't break a claim."""
|
|
129
|
+
ups = [(e["dst"], e["rel"]) for e in s.out.get(x, []) if e["rel"] in RISKY]
|
|
130
|
+
ups += [(e["src"], "produced_by") for e in s.inn.get(x, []) if e["rel"] == "produced"]
|
|
131
|
+
return ups
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def downstream(s: Snap, x: str) -> list[tuple[str, str]]:
|
|
135
|
+
"""What rests on x, one step: the inverse of upstream, plus what x supports."""
|
|
136
|
+
downs = [(e["src"], e["rel"]) for e in s.inn.get(x, []) if e["rel"] in RISKY]
|
|
137
|
+
downs += [(e["dst"], "produced") for e in s.out.get(x, []) if e["rel"] == "produced"]
|
|
138
|
+
downs += [(e["dst"], "supports") for e in s.out.get(x, []) if e["rel"] == "supports"]
|
|
139
|
+
return downs
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def walk(s: Snap, nid: str, step, limit: int) -> list[tuple[str, str, int]]:
|
|
143
|
+
seen, out, frontier = {nid}, [], [(nid, 0)]
|
|
144
|
+
while frontier and len(out) < limit:
|
|
145
|
+
nxt = []
|
|
146
|
+
for x, d in frontier:
|
|
147
|
+
for y, rel in step(s, x):
|
|
148
|
+
if y not in seen:
|
|
149
|
+
seen.add(y)
|
|
150
|
+
out.append((y, rel, d + 1))
|
|
151
|
+
nxt.append((y, d + 1))
|
|
152
|
+
frontier = nxt
|
|
153
|
+
return out
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def ancestors(s: Snap, nid: str, limit: int = 500) -> list[tuple[str, str, int]]:
|
|
157
|
+
return walk(s, nid, upstream, limit)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def dependents(s: Snap, nid: str, limit: int = 500) -> list[tuple[str, str, int]]:
|
|
161
|
+
return walk(s, nid, downstream, limit)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def supporters(s: Snap, nid: str) -> list[dict]:
|
|
165
|
+
return [e for e in s.inn.get(nid, []) if e["rel"] == "supports"]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def at_risk(s: Snap, nid: str) -> list[str]:
|
|
169
|
+
return list(dict.fromkeys(a for a, _r, _d in ancestors(s, nid) if fallen(s, a)))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def rests_on_superseded(s: Snap, nid: str) -> list[str]:
|
|
173
|
+
return [a for a, _r, _d in ancestors(s, nid) if lifecycle(s, a) == "superseded"]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def out_of_play(s: Snap, nid: str) -> bool:
|
|
177
|
+
return fallen(s, nid) or lifecycle(s, nid) == "superseded"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def unsupported(s: Snap, nid: str) -> bool:
|
|
181
|
+
sup = supporters(s, nid)
|
|
182
|
+
return bool(sup) and all(fallen(s, e["src"]) for e in sup)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def producing_run(s: Snap, rid: str) -> Optional[str]:
|
|
186
|
+
for e in s.inn.get(rid, []):
|
|
187
|
+
if e["rel"] == "produced":
|
|
188
|
+
return e["src"]
|
|
189
|
+
for e in s.out.get(rid, []):
|
|
190
|
+
if e["rel"] == "derived_from" and s.nodes[e["dst"]]["type"] == "run":
|
|
191
|
+
return e["dst"]
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def line_profile(s: Snap, src: str, depth: int = 0) -> str:
|
|
196
|
+
n = s.nodes[src]
|
|
197
|
+
if n["type"] == "source":
|
|
198
|
+
return "external"
|
|
199
|
+
if n["type"] == "claim":
|
|
200
|
+
return profile(s, src, depth + 1) if depth < 4 else "unverified"
|
|
201
|
+
run = producing_run(s, src)
|
|
202
|
+
if not run:
|
|
203
|
+
# "cited" counts as outside evidence only when it cites a source node; the word alone is a report
|
|
204
|
+
cites = any(e["rel"] == "cites" and s.nodes[e["dst"]]["type"] == "source" and not fallen(s, e["dst"])
|
|
205
|
+
for e in s.out.get(src, []))
|
|
206
|
+
return "external" if n["body"].get("kind") == "cited" and cites else "unverified"
|
|
207
|
+
rb = s.nodes[run]["body"]
|
|
208
|
+
# Lab-recorded means the instrument filed this result itself (only it adds `produced` from a lab run). A result
|
|
209
|
+
# someone derived from a lab run, or a run they call "external", is their report; outside evidence is a source.
|
|
210
|
+
by_lab = rb.get("kind") == "lab" and any(e["rel"] == "produced" and e["src"] == run for e in s.inn.get(src, []))
|
|
211
|
+
best = "lab_recorded" if by_lab else "agent_reported"
|
|
212
|
+
for e in s.out.get(run, []) + s.inn.get(run, []):
|
|
213
|
+
if e["rel"] == "replicates" and not fallen(s, e["src"]):
|
|
214
|
+
k = {"same_code": "project_rerun", "new_seed": "project_rerun", "new_env": "project_rerun",
|
|
215
|
+
"new_impl": "separate_implementation", "new_data": "new_data"}.get(e["data"].get("differs"),
|
|
216
|
+
"project_rerun")
|
|
217
|
+
if PROFILE_RANK.index(k) > PROFILE_RANK.index(best):
|
|
218
|
+
best = k
|
|
219
|
+
return best
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def profile(s: Snap, nid: str, depth: int = 0) -> str:
|
|
223
|
+
lines = [e["src"] for e in supporters(s, nid) if not fallen(s, e["src"])]
|
|
224
|
+
if not lines:
|
|
225
|
+
return "unverified"
|
|
226
|
+
return max((line_profile(s, x, depth) for x in lines), key=PROFILE_RANK.index)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def roots(s: Snap, src: str) -> set[str]:
|
|
230
|
+
"""What a line of evidence is made of: its run(s) and the data / model artifacts they used, its sources."""
|
|
231
|
+
out: set[str] = set()
|
|
232
|
+
stack, seen = [src], set()
|
|
233
|
+
while stack and len(seen) < 200:
|
|
234
|
+
x = stack.pop()
|
|
235
|
+
if x in seen:
|
|
236
|
+
continue
|
|
237
|
+
seen.add(x)
|
|
238
|
+
n = s.nodes[x]
|
|
239
|
+
if n["type"] == "run":
|
|
240
|
+
out.add(x)
|
|
241
|
+
for e in s.out.get(x, []):
|
|
242
|
+
if e["rel"] == "uses" and (e["data"].get("role") or "data") in ("data", "model"):
|
|
243
|
+
out.add(e["dst"])
|
|
244
|
+
continue
|
|
245
|
+
if n["type"] in ("artifact", "source"):
|
|
246
|
+
out.add(x)
|
|
247
|
+
continue
|
|
248
|
+
run = producing_run(s, x)
|
|
249
|
+
if run:
|
|
250
|
+
stack.append(run)
|
|
251
|
+
for e in s.out.get(x, []):
|
|
252
|
+
if e["rel"] in ("derived_from",):
|
|
253
|
+
stack.append(e["dst"])
|
|
254
|
+
if n["type"] == "claim":
|
|
255
|
+
for e in supporters(s, x):
|
|
256
|
+
stack.append(e["src"])
|
|
257
|
+
# No recorded run, data or source behind it: we can't tell these lines apart, so they all count as one
|
|
258
|
+
# (conservative — three unprovenanced reports may well be one result repeated).
|
|
259
|
+
return out or {"(no provenance)"}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def independent_lines(s: Snap, nid: str) -> int:
|
|
263
|
+
"""Support lines that share no run, data artifact or source count separately; any overlap merges them."""
|
|
264
|
+
lines = [e["src"] for e in supporters(s, nid) if not fallen(s, e["src"])]
|
|
265
|
+
groups: list[set] = []
|
|
266
|
+
for x in lines:
|
|
267
|
+
r = roots(s, x)
|
|
268
|
+
merged = [g for g in groups if g & r]
|
|
269
|
+
for g in merged:
|
|
270
|
+
groups.remove(g)
|
|
271
|
+
r |= g
|
|
272
|
+
groups.append(r)
|
|
273
|
+
return len(groups)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def basis_changed(s: Snap, nid: str) -> list[dict]:
|
|
277
|
+
"""A decision's basis that is no longer what the decision saw: a node revised since the decision cited it."""
|
|
278
|
+
out = []
|
|
279
|
+
for e in s.out.get(nid, []):
|
|
280
|
+
if e["rel"] != "based_on":
|
|
281
|
+
continue
|
|
282
|
+
cur = s.nodes[e["dst"]]["v"]
|
|
283
|
+
if e["dst_v"] and cur > e["dst_v"]:
|
|
284
|
+
out.append({"node": e["dst"], "code": s.nodes[e["dst"]]["code"], "was_v": e["dst_v"], "now_v": cur})
|
|
285
|
+
return out
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# What a node's standing reads. Out-edges: what it rests on or is made of. In-edges: what reviews, supports,
|
|
289
|
+
# challenges, supersedes or produced it. A change at y can alter the standing of x only if x reads y this way —
|
|
290
|
+
# directly, or through a chain (at-risk and evidence are transitive).
|
|
291
|
+
_READS_OUT = {"requires", "derived_from", "based_on", "cites", "uses", "replicates"}
|
|
292
|
+
_READS_IN = {"reviews", "supports", "challenges", "supersedes", "corrects", "produced", "replicates"}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def affected_by(s: Snap, touched: set) -> set:
|
|
296
|
+
"""Every node whose derived standing can have changed when ``touched`` changed: the touched nodes and,
|
|
297
|
+
transitively, everything that reads them. A touched investigation (its reviewers, strictness, state) affects
|
|
298
|
+
every node in it. Used to keep cached standing across a write instead of re-deriving the whole graph."""
|
|
299
|
+
out: set = set()
|
|
300
|
+
stack = [x for x in touched if x in s.nodes]
|
|
301
|
+
invs = {x for x in stack if s.nodes[x]["type"] == "investigation"}
|
|
302
|
+
if invs:
|
|
303
|
+
stack += [i for i, n in s.nodes.items() if n["inv"] in invs]
|
|
304
|
+
while stack:
|
|
305
|
+
y = stack.pop()
|
|
306
|
+
if y in out:
|
|
307
|
+
continue
|
|
308
|
+
out.add(y)
|
|
309
|
+
stack += [e["src"] for e in s.inn.get(y, ()) if e["rel"] in _READS_OUT and e["src"] not in out]
|
|
310
|
+
stack += [e["dst"] for e in s.out.get(y, ()) if e["rel"] in _READS_IN and e["dst"] not in out]
|
|
311
|
+
return out
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def state(s: Snap, nid: str) -> dict:
|
|
315
|
+
"""Every standing dimension of a node, plus one honest sentence. Cached on the snapshot."""
|
|
316
|
+
if nid in s._state:
|
|
317
|
+
return s._state[nid]
|
|
318
|
+
n = s.nodes[nid]
|
|
319
|
+
t = n["type"]
|
|
320
|
+
st: dict[str, Any] = {"lifecycle": lifecycle(s, nid)}
|
|
321
|
+
if t in ("claim", "result", "run", "experiment", "hypothesis"):
|
|
322
|
+
st["review"] = review_state(s, nid)
|
|
323
|
+
if t == "run":
|
|
324
|
+
st["execution"] = n["body"].get("state") or "unknown"
|
|
325
|
+
if t == "result":
|
|
326
|
+
st["outcome"] = n["body"].get("outcome") or "unstated"
|
|
327
|
+
if t in ("claim", "hypothesis"):
|
|
328
|
+
st["evidence"] = profile(s, nid)
|
|
329
|
+
st["independent_lines"] = independent_lines(s, nid)
|
|
330
|
+
st["challenges"] = sum(1 for e in s.inn.get(nid, []) if e["rel"] == "challenges" and not fallen(s, e["src"]))
|
|
331
|
+
st["unsupported"] = unsupported(s, nid)
|
|
332
|
+
if t == "decision":
|
|
333
|
+
moved = basis_changed(s, nid)
|
|
334
|
+
if moved:
|
|
335
|
+
st["basis_changed"] = moved
|
|
336
|
+
risk = at_risk(s, nid)
|
|
337
|
+
if risk:
|
|
338
|
+
st["at_risk"] = risk
|
|
339
|
+
older = rests_on_superseded(s, nid)
|
|
340
|
+
if older:
|
|
341
|
+
st["rests_on_superseded"] = older
|
|
342
|
+
if nid in s.leases:
|
|
343
|
+
L = s.leases[nid]
|
|
344
|
+
st["lease"] = {"agent": L["agent"], "task": L["task"], "expires": L["expires"]}
|
|
345
|
+
st["fallen"] = fallen(s, nid)
|
|
346
|
+
st["sentence"] = sentence(s, nid, st)
|
|
347
|
+
s._state[nid] = st
|
|
348
|
+
return st
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def sentence(s: Snap, nid: str, st: dict) -> str:
|
|
352
|
+
"""The node's standing in one honest line."""
|
|
353
|
+
n = s.nodes[nid]
|
|
354
|
+
parts = [f"{st['lifecycle']} {n['type']}"]
|
|
355
|
+
rv = st.get("review")
|
|
356
|
+
if rv:
|
|
357
|
+
if rv["state"] == "checked":
|
|
358
|
+
parts.append("checked (" + ", ".join(rv["positive"]) + f", v{n['v']})")
|
|
359
|
+
elif rv["state"] == "contested":
|
|
360
|
+
who = [o["by"] for o in rv["open_objections"]] + rv["negative"]
|
|
361
|
+
parts.append("CONTESTED (" + ", ".join(who) + ")")
|
|
362
|
+
elif rv["state"] == "needs_revalidation":
|
|
363
|
+
parts.append(f"review stale — v{n['v']} not yet re-checked")
|
|
364
|
+
else:
|
|
365
|
+
parts.append("unchecked")
|
|
366
|
+
if "execution" in st:
|
|
367
|
+
parts.append(st["execution"])
|
|
368
|
+
if "outcome" in st:
|
|
369
|
+
parts.append(f"outcome {st['outcome']}")
|
|
370
|
+
if "evidence" in st:
|
|
371
|
+
k = st["independent_lines"]
|
|
372
|
+
parts.append(f"{k} independent line{'s' if k != 1 else ''} of support" if k else "no live support")
|
|
373
|
+
parts.append(PROFILE_LABEL[st["evidence"]])
|
|
374
|
+
if st["evidence"] != "external":
|
|
375
|
+
parts.append("no external replication")
|
|
376
|
+
if st.get("challenges"):
|
|
377
|
+
parts.append(f"{st['challenges']} challenge(s)")
|
|
378
|
+
if st.get("unsupported"):
|
|
379
|
+
parts.append("UNSUPPORTED — every support has fallen")
|
|
380
|
+
if st.get("basis_changed"):
|
|
381
|
+
parts.append("REVISIT — its basis changed: " + ", ".join(
|
|
382
|
+
f"{b['code']} v{b['was_v']}→v{b['now_v']}" for b in st["basis_changed"][:5]))
|
|
383
|
+
if st.get("at_risk"):
|
|
384
|
+
parts.append("AT RISK — rests on " + ", ".join(s.nodes[a]["code"] or a for a in st["at_risk"][:5]))
|
|
385
|
+
if st.get("rests_on_superseded"):
|
|
386
|
+
parts.append("rests on superseded " + ", ".join(s.nodes[a]["code"] or a for a in st["rests_on_superseded"][:5])
|
|
387
|
+
+ " — check it against the replacement")
|
|
388
|
+
if st.get("lease"):
|
|
389
|
+
parts.append(f"being worked by {st['lease']['agent']}")
|
|
390
|
+
return " · ".join(parts)
|