align-agent 0.4.3__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.
- align_agent/__init__.py +11 -0
- align_agent/_audit/common.py +58 -0
- align_agent/_audit/deepswe.py +260 -0
- align_agent/_audit/excerpts.py +203 -0
- align_agent/_audit/extract.py +798 -0
- align_agent/_audit/materialize.py +624 -0
- align_agent/_audit/score.py +325 -0
- align_agent/_audit/shellparse.py +1141 -0
- align_agent/_audit/tarsur.py +153 -0
- align_agent/_vendor/src/__init__.py +3 -0
- align_agent/_vendor/src/harness/__init__.py +13 -0
- align_agent/_vendor/src/harness/arms.py +524 -0
- align_agent/_vendor/src/harness/completion.py +104 -0
- align_agent/_vendor/src/harness/dependency_notice.py +616 -0
- align_agent/_vendor/src/harness/freshness_hook.py +131 -0
- align_agent/_vendor/src/harness/lean.py +41 -0
- align_agent/_vendor/src/harness/mini_real.py +1040 -0
- align_agent/_vendor/src/harness/miniswe_scaffold.py +661 -0
- align_agent/_vendor/src/harness/tail_diff.py +292 -0
- align_agent/_vendor/src/harness/viewport.py +220 -0
- align_agent/_vendor/src/metrics/__init__.py +38 -0
- align_agent/_vendor/src/metrics/coherence.py +354 -0
- align_agent/_vendor/src/metrics/corruption_oracles.py +139 -0
- align_agent/_vendor/src/metrics/replay.py +399 -0
- align_agent/_vendor/src/metrics/statistics.py +185 -0
- align_agent/_vendor/src/metrics/verification.py +104 -0
- align_agent/_vendor/src/parsers/__init__.py +33 -0
- align_agent/_vendor/src/parsers/base_parser.py +349 -0
- align_agent/_vendor/src/parsers/file_events.py +806 -0
- align_agent/_vendor/src/parsers/miniswe_parser.py +96 -0
- align_agent/_vendor/src/parsers/openhands_parser.py +414 -0
- align_agent/_vendor/src/parsers/sweagent_parser.py +180 -0
- align_agent/_vendor/src/parsers/terminus_parser.py +106 -0
- align_agent/cli.py +101 -0
- align_agent/core.py +161 -0
- align_agent/run.py +84 -0
- align_agent/sessions.py +985 -0
- align_agent/writethrough/__init__.py +4 -0
- align_agent/writethrough/_lib/__init__.py +1 -0
- align_agent/writethrough/_lib/wt_src/__init__.py +3 -0
- align_agent/writethrough/_lib/wt_src/harness/__init__.py +1 -0
- align_agent/writethrough/_lib/wt_src/harness/dependency_notice.py +616 -0
- align_agent/writethrough/_lib/wt_src/harness/lean.py +41 -0
- align_agent/writethrough/_lib/wt_src/harness/tail_diff.py +292 -0
- align_agent/writethrough/_lib/wt_src/harness/viewport.py +220 -0
- align_agent/writethrough/_lib/wt_src/metrics/__init__.py +38 -0
- align_agent/writethrough/_lib/wt_src/metrics/replay.py +399 -0
- align_agent/writethrough/_lib/wt_src/parsers/__init__.py +1 -0
- align_agent/writethrough/_lib/wt_src/parsers/file_events.py +806 -0
- align_agent/writethrough/claude_code.py +81 -0
- align_agent/writethrough/engine.py +647 -0
- align_agent/writethrough/hooks.py +227 -0
- align_agent/writethrough/mcp_server.py +125 -0
- align_agent/writethrough/protocol.py +68 -0
- align_agent-0.4.3.dist-info/METADATA +224 -0
- align_agent-0.4.3.dist-info/RECORD +60 -0
- align_agent-0.4.3.dist-info/WHEEL +5 -0
- align_agent-0.4.3.dist-info/entry_points.txt +6 -0
- align_agent-0.4.3.dist-info/licenses/LICENSE +21 -0
- align_agent-0.4.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Independent stale-edit scorer (version-level and region-level). Usage: python3 scripts/audit/score.py
|
|
2
|
+
|
|
3
|
+
Reads docs/audit/rq1/work/extracted.jsonl (from extract.py) and replays each
|
|
4
|
+
trajectory. Definitions (the RQ1 structural definition, re-implemented):
|
|
5
|
+
|
|
6
|
+
content id every landed write gives the file a new content id; a cp/mv
|
|
7
|
+
gives the destination the source's id; a git restore returns
|
|
8
|
+
the base id 0; an editor undo returns the id before the last
|
|
9
|
+
editor write.
|
|
10
|
+
view content reaching the agent: a read op, an editor echo, or a
|
|
11
|
+
file quoted in the output (grep -rn / compiler `path:N:` lines,
|
|
12
|
+
traceback frames, diff headers). A directory listing is not a
|
|
13
|
+
view. Views count from the next composition unit on.
|
|
14
|
+
opportunity the first anchored edit to file f in a composition unit, when
|
|
15
|
+
the agent had seen some content of f before that unit.
|
|
16
|
+
hazard an opportunity where f's content id at the edit is not among
|
|
17
|
+
the ids the agent had been shown (or had authored) before it.
|
|
18
|
+
|
|
19
|
+
Variants: without editor echoes; without output-derived views; landed=None
|
|
20
|
+
treated as failed; per-step (not per-unit) composition; and a region-level
|
|
21
|
+
variant in which a view refreshes only the lines it showed.
|
|
22
|
+
Writes docs/audit/rq1/work/score_summary.json and hazards.jsonl.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import re
|
|
28
|
+
import sys
|
|
29
|
+
from collections import Counter, defaultdict
|
|
30
|
+
|
|
31
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
32
|
+
sys.path.insert(0, HERE)
|
|
33
|
+
from common import WORK, short_model # noqa: E402
|
|
34
|
+
|
|
35
|
+
OUT_TRACE = re.compile(r'File "([^"<>\n]+)", line (\d+)')
|
|
36
|
+
OUT_LOC = re.compile(r"(?m)^\s*(?:-->\s+)?((?:/|\./|[\w.\-]+/)?(?:[\w.\-]+/)*[\w.\-]+\.[A-Za-z0-9]{1,8}):(\d+)[:\s-]")
|
|
37
|
+
OUT_DIFF = re.compile(r"(?m)^(?:diff --git a/(\S+) b/|\+\+\+ b/(\S+))")
|
|
38
|
+
# Blocks the write-through hook (src/harness) appends to a command's output.
|
|
39
|
+
# Each shows the file's current state for its region, so it is a view.
|
|
40
|
+
OUT_HOOK = re.compile(r"(?m)^\[(?:TAIL DIFF: File '([^'\n]+)'|VIEWPORT \(lines \d+-\d+ of \d+\): ([^\]\n]+))")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def canon_path(p, cwd, root, known=None):
|
|
44
|
+
"""Canonical spelling of a path quoted in tool output.
|
|
45
|
+
|
|
46
|
+
Compilers, test runners and diff headers quote paths relative to the
|
|
47
|
+
project root (cargo, pytest, git), not to the shell's cwd; tracebacks
|
|
48
|
+
quote absolute paths. Candidates are tried in order and the first one
|
|
49
|
+
naming a file the trajectory touches elsewhere wins: root-relative,
|
|
50
|
+
cwd-relative, then a unique known path ending with the quoted path."""
|
|
51
|
+
import posixpath
|
|
52
|
+
if not p:
|
|
53
|
+
return None
|
|
54
|
+
p = p.strip()
|
|
55
|
+
if p.startswith("/proc/self/cwd/"):
|
|
56
|
+
p = posixpath.join(cwd or "/", p[len("/proc/self/cwd/"):])
|
|
57
|
+
if p.startswith(("a/", "b/")) and not p.startswith("/"):
|
|
58
|
+
p = p[2:]
|
|
59
|
+
if p.startswith("./"):
|
|
60
|
+
p = p[2:]
|
|
61
|
+
|
|
62
|
+
def strip(ap):
|
|
63
|
+
if root and (ap == root or ap.startswith(root + "/")):
|
|
64
|
+
return ap[len(root) + 1:]
|
|
65
|
+
return ap
|
|
66
|
+
|
|
67
|
+
if p.startswith("/"):
|
|
68
|
+
cands = [strip(posixpath.normpath(p))]
|
|
69
|
+
else:
|
|
70
|
+
cands = [posixpath.normpath(p),
|
|
71
|
+
strip(posixpath.normpath(posixpath.join(cwd or "/", p)))]
|
|
72
|
+
if known:
|
|
73
|
+
for c in cands:
|
|
74
|
+
if c in known:
|
|
75
|
+
return c
|
|
76
|
+
tail = [k for k in known if k.endswith("/" + cands[0])]
|
|
77
|
+
if len(tail) == 1:
|
|
78
|
+
return tail[0]
|
|
79
|
+
return cands[-1] if p.startswith("/") else cands[1]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def output_views(step, root, known=None):
|
|
83
|
+
"""Files quoted in the step's output, with the line numbers quoted."""
|
|
84
|
+
obs = step.get("obs") or ""
|
|
85
|
+
if len(obs) > 400000:
|
|
86
|
+
obs = obs[:400000]
|
|
87
|
+
found = defaultdict(set)
|
|
88
|
+
for m in OUT_TRACE.finditer(obs):
|
|
89
|
+
found[m.group(1)].add(int(m.group(2)))
|
|
90
|
+
for m in OUT_LOC.finditer(obs):
|
|
91
|
+
found[m.group(1)].add(int(m.group(2)))
|
|
92
|
+
for m in OUT_DIFF.finditer(obs):
|
|
93
|
+
found[m.group(1) or m.group(2)].add(0)
|
|
94
|
+
for m in OUT_HOOK.finditer(obs):
|
|
95
|
+
found[m.group(1) or m.group(2)].add(0)
|
|
96
|
+
out = {}
|
|
97
|
+
for p, lines in found.items():
|
|
98
|
+
if p.startswith(("/usr/", "/opt/", "/root/.", "<")) or "site-packages" in p or "/lib/python" in p:
|
|
99
|
+
continue
|
|
100
|
+
c = canon_path(p, step.get("cwd"), root, known)
|
|
101
|
+
if c:
|
|
102
|
+
out[c] = lines
|
|
103
|
+
return out
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def known_paths(traj):
|
|
107
|
+
return {o["path"] for s in traj["steps"] for o in s["ops"]}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class Replay:
|
|
111
|
+
def __init__(self, traj, echo=True, diag=True, none_is_landed=True, per_step=False):
|
|
112
|
+
self.t = traj
|
|
113
|
+
self.echo, self.diag, self.none_landed, self.per_step = echo, diag, none_is_landed, per_step
|
|
114
|
+
self.next_id = 1
|
|
115
|
+
self.cur = {} # (conv, path) -> content id
|
|
116
|
+
self.seen = defaultdict(set)
|
|
117
|
+
self.editor_hist = defaultdict(list)
|
|
118
|
+
self.modified = set()
|
|
119
|
+
self.views = defaultdict(list) # (conv,path) -> [(step_i, cid, kind, op_index)]
|
|
120
|
+
self.writes = defaultdict(list) # (conv,path) -> [(step_i, op_index, cid_before, cid_after, landed)]
|
|
121
|
+
self.opps = []
|
|
122
|
+
self.hazards = []
|
|
123
|
+
self.intra = [] # later anchored edits in the same unit after a landed write to the file
|
|
124
|
+
self.not_executed = [] # anchored edit commands that never ran (not found, syntax error)
|
|
125
|
+
self.known = known_paths(traj)
|
|
126
|
+
|
|
127
|
+
def cid(self, k):
|
|
128
|
+
return self.cur.get(k, 0)
|
|
129
|
+
|
|
130
|
+
def new(self):
|
|
131
|
+
self.next_id += 1
|
|
132
|
+
return self.next_id
|
|
133
|
+
|
|
134
|
+
def run(self):
|
|
135
|
+
steps = self.t["steps"]
|
|
136
|
+
root = self.t.get("root")
|
|
137
|
+
groups = []
|
|
138
|
+
for s in steps:
|
|
139
|
+
key = (s["conv"], s["i"]) if self.per_step else (s["conv"], s["unit"])
|
|
140
|
+
if groups and groups[-1][0] == key:
|
|
141
|
+
groups[-1][1].append(s)
|
|
142
|
+
else:
|
|
143
|
+
groups.append((key, [s]))
|
|
144
|
+
for (conv, unit), members in groups:
|
|
145
|
+
held = {k: set(v) for k, v in self.seen.items() if k[0] == conv}
|
|
146
|
+
scored = set()
|
|
147
|
+
written_in_unit = set()
|
|
148
|
+
pending_views = []
|
|
149
|
+
for s in members:
|
|
150
|
+
for oi, o in enumerate(s["ops"]):
|
|
151
|
+
k = (conv, o["path"])
|
|
152
|
+
if o["op"] == "R":
|
|
153
|
+
if o.get("landed") is False or o.get("kind") == "listing":
|
|
154
|
+
continue
|
|
155
|
+
if o.get("kind") == "echo" and not self.echo:
|
|
156
|
+
continue
|
|
157
|
+
pending_views.append((k, self.cid(k), s["i"], o.get("kind"), oi))
|
|
158
|
+
continue
|
|
159
|
+
landed = o.get("landed")
|
|
160
|
+
lands = landed is True or (landed is None and self.none_landed)
|
|
161
|
+
if o["path"] == "*":
|
|
162
|
+
if lands and o["kind"] == "restore":
|
|
163
|
+
for kk in list(self.modified):
|
|
164
|
+
if kk[0] == conv:
|
|
165
|
+
before = self.cid(kk)
|
|
166
|
+
self.cur[kk] = 0
|
|
167
|
+
self.writes[kk].append((s["i"], oi, before, 0, landed, "restore*", unit))
|
|
168
|
+
continue
|
|
169
|
+
if o["kind"] == "anchored" and o.get("ran") is False:
|
|
170
|
+
self.not_executed.append({"step": s["i"], "op": oi, "path": o["path"], "ev": o.get("ev")})
|
|
171
|
+
elif o["kind"] == "anchored":
|
|
172
|
+
if k not in scored:
|
|
173
|
+
scored.add(k)
|
|
174
|
+
if k in held:
|
|
175
|
+
c = self.cid(k)
|
|
176
|
+
haz = c not in held[k]
|
|
177
|
+
lv = self.views[k][-1] if self.views[k] else None
|
|
178
|
+
lv_pos = (lv[0], 10 ** 6 if lv[3] == -1 else lv[3]) if lv else (-1, -1)
|
|
179
|
+
W = [w for w in self.writes[k] if lv_pos < (w[0], w[1]) < (s["i"], oi)]
|
|
180
|
+
rec = {"step": s["i"], "op": oi, "path": o["path"], "conv": conv, "unit": unit,
|
|
181
|
+
"bench": s.get("bench"), "hazard": haz, "landed": landed,
|
|
182
|
+
"mech": o.get("mech"), "at_risk": any(w[3] != w[2] for w in self.writes[k]),
|
|
183
|
+
"last_view": lv, "W": W, "n_views_before": len(self.views[k])}
|
|
184
|
+
self.opps.append(rec)
|
|
185
|
+
if haz:
|
|
186
|
+
self.hazards.append(rec)
|
|
187
|
+
elif k in written_in_unit:
|
|
188
|
+
prior = [w for w in self.writes[k] if w[6] == unit]
|
|
189
|
+
self.intra.append({"step": s["i"], "op": oi, "path": o["path"], "conv": conv, "unit": unit,
|
|
190
|
+
"bench": s.get("bench"), "landed": landed, "mech": o.get("mech"),
|
|
191
|
+
"prior_same_unit_writes": prior})
|
|
192
|
+
if not lands:
|
|
193
|
+
continue
|
|
194
|
+
before = self.cid(k)
|
|
195
|
+
kind = o["kind"]
|
|
196
|
+
if kind == "restore":
|
|
197
|
+
if k in self.modified:
|
|
198
|
+
self.cur[k] = 0
|
|
199
|
+
elif kind == "touch":
|
|
200
|
+
continue
|
|
201
|
+
elif kind == "replace" and o.get("src"):
|
|
202
|
+
sk = (conv, o["src"])
|
|
203
|
+
self.cur[k] = self.cid(sk) if sk in self.cur or sk in self.seen else self.new()
|
|
204
|
+
if self.cur[k] == 0:
|
|
205
|
+
self.cur[k] = ("base", o["src"])
|
|
206
|
+
elif kind == "undo":
|
|
207
|
+
hist = self.editor_hist[k]
|
|
208
|
+
self.cur[k] = hist.pop() if hist else self.new()
|
|
209
|
+
else:
|
|
210
|
+
self.cur[k] = self.new()
|
|
211
|
+
if o.get("mech", "").startswith("editor"):
|
|
212
|
+
self.editor_hist[k].append(before)
|
|
213
|
+
self.modified.add(k)
|
|
214
|
+
written_in_unit.add(k)
|
|
215
|
+
self.writes[k].append((s["i"], oi, before, self.cid(k), landed, kind, unit))
|
|
216
|
+
if kind == "overwrite":
|
|
217
|
+
# the agent authored every byte: a current view
|
|
218
|
+
held.setdefault(k, set()).add(self.cur[k])
|
|
219
|
+
pending_views.append((k, self.cur[k], s["i"], "authored", oi))
|
|
220
|
+
if self.diag:
|
|
221
|
+
for p, lines in output_views(s, root, self.known).items():
|
|
222
|
+
k = (conv, p)
|
|
223
|
+
pending_views.append((k, self.cid(k), s["i"], "diag", -1))
|
|
224
|
+
for k, c, si, kind, oi in pending_views:
|
|
225
|
+
self.seen[k].add(c)
|
|
226
|
+
self.views[k].append((si, c, kind, oi, unit))
|
|
227
|
+
return self
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def cp_ci(k, n, a=0.05):
|
|
231
|
+
from scipy.stats import beta
|
|
232
|
+
if n == 0:
|
|
233
|
+
return (None, None)
|
|
234
|
+
lo = 0.0 if k == 0 else beta.ppf(a / 2, k, n - k + 1)
|
|
235
|
+
hi = 1.0 if k == n else beta.ppf(1 - a / 2, k + 1, n - k)
|
|
236
|
+
return (round(float(lo), 4), round(float(hi), 4))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def cell(t):
|
|
240
|
+
fw = t["agent"]
|
|
241
|
+
if fw == "OpenHands":
|
|
242
|
+
fw = "OpenHands"
|
|
243
|
+
return fw, short_model(t["model"])
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def main():
|
|
247
|
+
variants = {
|
|
248
|
+
"primary": dict(),
|
|
249
|
+
"no_echo": dict(echo=False),
|
|
250
|
+
"no_diag": dict(diag=False),
|
|
251
|
+
"none_as_failed": dict(none_is_landed=False),
|
|
252
|
+
"per_step": dict(per_step=True),
|
|
253
|
+
}
|
|
254
|
+
tallies = {v: defaultdict(Counter) for v in variants}
|
|
255
|
+
per_traj = []
|
|
256
|
+
hz_out = open(os.path.join(WORK, "hazards.jsonl"), "w", encoding="utf-8")
|
|
257
|
+
opp_out = open(os.path.join(WORK, "opportunities.jsonl"), "w", encoding="utf-8")
|
|
258
|
+
intra_out = open(os.path.join(WORK, "intra_unit.jsonl"), "w", encoding="utf-8")
|
|
259
|
+
with open(os.path.join(WORK, "extracted.jsonl"), encoding="utf-8") as f:
|
|
260
|
+
for line in f:
|
|
261
|
+
t = json.loads(line)
|
|
262
|
+
if t.get("missing"):
|
|
263
|
+
continue
|
|
264
|
+
c = cell(t)
|
|
265
|
+
row = {"traj_id": t["traj_id"], "agent": t["agent"], "model": short_model(t["model"]),
|
|
266
|
+
"layout": t.get("layout"), "steps": t["n_steps"], "n_conv": t.get("n_conv")}
|
|
267
|
+
for v, kw in variants.items():
|
|
268
|
+
r = Replay(t, **kw).run()
|
|
269
|
+
tal = tallies[v][c]
|
|
270
|
+
tal["trajectories"] += 1
|
|
271
|
+
tal["steps"] += t["n_steps"]
|
|
272
|
+
tal["opportunities"] += len(r.opps)
|
|
273
|
+
tal["hazards"] += len(r.hazards)
|
|
274
|
+
tal["at_risk_opportunities"] += sum(1 for o in r.opps if o["at_risk"])
|
|
275
|
+
tal["zero_opp_trajectories"] += not r.opps
|
|
276
|
+
tal["hazard_trajectories"] += bool(r.hazards)
|
|
277
|
+
tal["intra_unit_repeat_edits"] += len(r.intra)
|
|
278
|
+
tal["anchored_not_executed"] += len(r.not_executed)
|
|
279
|
+
row[f"{v}_opps"] = len(r.opps)
|
|
280
|
+
row[f"{v}_haz"] = len(r.hazards)
|
|
281
|
+
if v == "primary":
|
|
282
|
+
meta = dict(traj_id=t["traj_id"], agent=t["agent"], model=short_model(t["model"]),
|
|
283
|
+
layout=t.get("layout"))
|
|
284
|
+
for o in r.opps:
|
|
285
|
+
opp_out.write(json.dumps(dict(o, **meta), default=str) + "\n")
|
|
286
|
+
for h in r.hazards:
|
|
287
|
+
hz_out.write(json.dumps(dict(h, **meta), default=str) + "\n")
|
|
288
|
+
for x in r.intra:
|
|
289
|
+
intra_out.write(json.dumps(dict(x, **meta), default=str) + "\n")
|
|
290
|
+
row["intra"] = len(r.intra)
|
|
291
|
+
ops = Counter()
|
|
292
|
+
for s in t["steps"]:
|
|
293
|
+
for o in s["ops"]:
|
|
294
|
+
ops[(o["op"], o["kind"])] += 1
|
|
295
|
+
row["ops"] = {f"{a}:{b}": n for (a, b), n in ops.items()}
|
|
296
|
+
row["zero_op_steps"] = sum(1 for s in t["steps"] if not s["ops"])
|
|
297
|
+
per_traj.append(row)
|
|
298
|
+
hz_out.close()
|
|
299
|
+
opp_out.close()
|
|
300
|
+
intra_out.close()
|
|
301
|
+
summary = {}
|
|
302
|
+
for v, cells in tallies.items():
|
|
303
|
+
summary[v] = {}
|
|
304
|
+
for (fw, m), tal in sorted(cells.items()):
|
|
305
|
+
n, k = tal["opportunities"], tal["hazards"]
|
|
306
|
+
summary[v][f"{fw}|{m}"] = dict(tal, rate=round(k / n, 4) if n else None, ci=cp_ci(k, n))
|
|
307
|
+
# framework totals
|
|
308
|
+
fwt = defaultdict(Counter)
|
|
309
|
+
for (fw, m), tal in cells.items():
|
|
310
|
+
fwt[fw].update(tal)
|
|
311
|
+
for fw, tal in fwt.items():
|
|
312
|
+
n, k = tal["opportunities"], tal["hazards"]
|
|
313
|
+
summary[v][f"{fw}|ALL"] = dict(tal, rate=round(k / n, 4) if n else None, ci=cp_ci(k, n))
|
|
314
|
+
with open(os.path.join(WORK, "score_summary.json"), "w") as f:
|
|
315
|
+
json.dump({"summary": summary, "per_trajectory": per_traj}, f, indent=1)
|
|
316
|
+
for key, v in summary["primary"].items():
|
|
317
|
+
print(f"{key:40s} traj {v['trajectories']:4d} steps {v['steps']:6d} opp {v['opportunities']:5d} "
|
|
318
|
+
f"haz {v['hazards']:5d} rate {v['rate']} ci {v['ci']} zeroOpp {v['zero_opp_trajectories']}")
|
|
319
|
+
print("--- variants (hazards/opportunities by framework)")
|
|
320
|
+
for var in variants:
|
|
321
|
+
print(var, {k: (v["hazards"], v["opportunities"]) for k, v in summary[var].items() if k.endswith("|ALL")})
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
if __name__ == "__main__":
|
|
325
|
+
main()
|