execweave 0.6.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.
Files changed (49) hide show
  1. execweave/__init__.py +3 -0
  2. execweave/__main__.py +5 -0
  3. execweave/analysis.py +406 -0
  4. execweave/backends.py +63 -0
  5. execweave/benchmark.py +80 -0
  6. execweave/claude_adapter.py +448 -0
  7. execweave/claude_hook_cli.py +101 -0
  8. execweave/claude_record.py +106 -0
  9. execweave/cli.py +588 -0
  10. execweave/codex_adapter.py +314 -0
  11. execweave/codex_hook_cli.py +98 -0
  12. execweave/codex_record.py +111 -0
  13. execweave/collector.py +301 -0
  14. execweave/correlation.py +604 -0
  15. execweave/cursor_adapter.py +347 -0
  16. execweave/cursor_hook_cli.py +82 -0
  17. execweave/cursor_record.py +96 -0
  18. execweave/filesystem.py +103 -0
  19. execweave/focus.py +118 -0
  20. execweave/gemini_adapter.py +265 -0
  21. execweave/gemini_hook_cli.py +77 -0
  22. execweave/gemini_record.py +94 -0
  23. execweave/graph.py +300 -0
  24. execweave/graph_ops.py +446 -0
  25. execweave/inference_gateway.py +422 -0
  26. execweave/inference_gateway_cli.py +106 -0
  27. execweave/inference_identity.py +76 -0
  28. execweave/inference_identity_cli.py +60 -0
  29. execweave/live.py +275 -0
  30. execweave/model_runtime.py +535 -0
  31. execweave/model_runtime_cli.py +154 -0
  32. execweave/opencode_adapter.py +316 -0
  33. execweave/opencode_hook_cli.py +57 -0
  34. execweave/opencode_plugin_cli.py +110 -0
  35. execweave/opencode_record.py +96 -0
  36. execweave/overhead_benchmark.py +440 -0
  37. execweave/provider_record.py +215 -0
  38. execweave/schema.py +62 -0
  39. execweave/semantic.py +346 -0
  40. execweave/sink.py +33 -0
  41. execweave/strace_backend.py +682 -0
  42. execweave/validate.py +193 -0
  43. execweave/viewer.py +283 -0
  44. execweave/workflow.py +114 -0
  45. execweave-0.6.0.dist-info/METADATA +356 -0
  46. execweave-0.6.0.dist-info/RECORD +49 -0
  47. execweave-0.6.0.dist-info/WHEEL +4 -0
  48. execweave-0.6.0.dist-info/entry_points.txt +17 -0
  49. execweave-0.6.0.dist-info/licenses/LICENSE +21 -0
execweave/live.py ADDED
@@ -0,0 +1,275 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import threading
5
+ import time
6
+ import webbrowser
7
+ from collections.abc import Callable
8
+ from dataclasses import dataclass
9
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
10
+ from pathlib import Path
11
+ from socketserver import TCPServer
12
+ from uuid import uuid4
13
+
14
+ from .backends import create_collector
15
+ from .graph import build_execution_graph, write_execution_graph
16
+ from .sink import JsonlSink
17
+ from .validate import validate_event_stream
18
+ from .viewer import render_graph_html, write_graph_html
19
+
20
+
21
+ _LIVE_HTML = r"""<!doctype html>
22
+ <html lang="en">
23
+ <head>
24
+ <meta charset="utf-8">
25
+ <meta name="viewport" content="width=device-width,initial-scale=1">
26
+ <title>ExecWeave Live</title>
27
+ <style>
28
+ :root{color-scheme:dark;--bg:#0b0f14;--panel:#111821;--panel2:#18222e;--text:#e8edf3;--muted:#8ea0b5;--border:#2a3949;--edge:#72869c;--causal:#70d6a6;--noncausal:#f2b76d;--selected:#73b7ff}
29
+ *{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:var(--bg);color:var(--text);font:14px/1.4 system-ui,sans-serif}
30
+ #app{display:grid;grid-template-columns:minmax(0,1fr) 320px;grid-template-rows:54px minmax(0,1fr);width:100%;height:100%}
31
+ header{grid-column:1/3;display:flex;align-items:center;gap:12px;padding:0 14px;background:var(--panel);border-bottom:1px solid var(--border)}
32
+ #status{border:1px solid var(--border);border-radius:999px;padding:3px 8px;color:var(--causal)}#stats{color:var(--muted);white-space:nowrap}
33
+ header input{margin-left:auto;width:min(420px,36vw);padding:7px 9px;border:1px solid var(--border);border-radius:7px;background:var(--panel2);color:var(--text)}
34
+ #wrap{position:relative;overflow:hidden}svg{width:100%;height:100%;display:block;cursor:grab;user-select:none}svg.panning{cursor:grabbing}
35
+ aside{overflow:auto;border-left:1px solid var(--border);background:var(--panel);padding:13px}aside pre{white-space:pre-wrap;overflow-wrap:anywhere;font:12px/1.45 ui-monospace,monospace}
36
+ .controls{position:absolute;left:10px;top:10px;z-index:3}.controls button{padding:6px 9px;border:1px solid var(--border);border-radius:7px;background:var(--panel);color:var(--text);cursor:pointer}
37
+ .node rect{stroke:var(--border);stroke-width:1.1;rx:8;ry:8}.node text{fill:var(--text);pointer-events:none}.node .type{fill:var(--muted);font-size:9px}.node.dim{opacity:.12}
38
+ .edge{fill:none;stroke:var(--edge);stroke-width:1.3;opacity:.72;marker-end:url(#arrow)}.edge.causal{stroke:var(--causal)}.edge.noncausal{stroke:var(--noncausal);stroke-dasharray:6 5}.edge.dim{opacity:.06}.edge-hit{fill:none;stroke:transparent;stroke-width:10;cursor:pointer}.label{fill:var(--muted);font-size:8.5px;pointer-events:none}
39
+ @media(max-width:820px){#app{grid-template-columns:1fr;grid-template-rows:54px minmax(0,1fr) 190px}header{grid-column:1}aside{border-left:0;border-top:1px solid var(--border)}}
40
+ </style>
41
+ </head>
42
+ <body>
43
+ <div id="app">
44
+ <header><strong>ExecWeave Live</strong><span id="status">LIVE</span><span id="stats">Waiting for events…</span><input id="search" placeholder="Search node id, name, type…"></header>
45
+ <div id="wrap"><div class="controls"><button id="fit">Fit</button></div><svg id="svg"><defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0 L10 5 L0 10 z" fill="context-stroke"></path></marker></defs><g id="viewport"><g id="edges"></g><g id="labels"></g><g id="nodes"></g></g></svg></div>
46
+ <aside><strong>Selection</strong><div id="details" style="color:var(--muted);margin-top:10px">Click a node or edge.</div></aside>
47
+ </div>
48
+ <script>
49
+ (()=>{
50
+ const svg=document.getElementById('svg'),viewport=document.getElementById('viewport'),edgeLayer=document.getElementById('edges'),labelLayer=document.getElementById('labels'),nodeLayer=document.getElementById('nodes'),details=document.getElementById('details'),search=document.getElementById('search'),stats=document.getElementById('stats'),status=document.getElementById('status');
51
+ let graph={nodes:[],edges:[]},positions=new Map(),transform={x:35,y:35,scale:1},pan=null,lastSignature='';
52
+ function color(type){let h=0;for(const c of String(type||'unknown'))h=((h<<5)-h+c.charCodeAt(0))|0;return `hsl(${Math.abs(h)%360} 38% 27%)`}
53
+ function short(node){const value=node.name||node.id||node.type||'node';return value.length>28?value.slice(0,25)+'…':value}
54
+ function layout(){const ids=graph.nodes.map(n=>n.id),nodeMap=new Map(graph.nodes.map(n=>[n.id,n])),incoming=new Map(ids.map(id=>[id,0])),outgoing=new Map(ids.map(id=>[id,[]]));for(const e of graph.edges){if(!nodeMap.has(e.source)||!nodeMap.has(e.target))continue;incoming.set(e.target,(incoming.get(e.target)||0)+1);outgoing.get(e.source).push(e.target)}let roots=ids.filter(id=>(incoming.get(id)||0)===0);if(!roots.length&&ids.length)roots=[ids[0]];const depth=new Map(),queue=roots.map(id=>[id,0]);for(let i=0;i<queue.length;i++){const [id,d]=queue[i];if(depth.has(id))continue;depth.set(id,d);for(const next of outgoing.get(id)||[])queue.push([next,d+1])}const max=Math.max(0,...depth.values());for(const id of ids)if(!depth.has(id))depth.set(id,max+1);const layers=new Map();for(const id of ids){const d=depth.get(id);if(!layers.has(d))layers.set(d,[]);layers.get(d).push(id)}for(const [d,list] of layers){list.sort((a,b)=>String(nodeMap.get(a)?.type).localeCompare(String(nodeMap.get(b)?.type))||a.localeCompare(b));list.forEach((id,i)=>{if(!positions.has(id))positions.set(id,{x:d*235,y:i*76})})}}
55
+ function anchor(id,right){const p=positions.get(id)||{x:0,y:0};return{x:p.x+(right?160:0),y:p.y+25}}
56
+ function curve(e){const a=anchor(e.source,true),b=anchor(e.target,false),bend=Math.max(38,Math.abs(b.x-a.x)*.42);return `M ${a.x} ${a.y} C ${a.x+bend} ${a.y}, ${b.x-bend} ${b.y}, ${b.x} ${b.y}`}
57
+ function show(value){details.innerHTML='';const pre=document.createElement('pre');pre.textContent=JSON.stringify(value,null,2);details.appendChild(pre)}
58
+ function render(){layout();edgeLayer.replaceChildren();labelLayer.replaceChildren();nodeLayer.replaceChildren();for(const e of graph.edges){if(!positions.has(e.source)||!positions.has(e.target))continue;const visible=document.createElementNS('http://www.w3.org/2000/svg','path');visible.setAttribute('d',curve(e));visible.classList.add('edge');if(e.causal===true)visible.classList.add('causal');else if(e.causal===false)visible.classList.add('noncausal');visible.dataset.source=e.source;visible.dataset.target=e.target;visible.dataset.relation=String(e.relation||'').toLowerCase();edgeLayer.appendChild(visible);const hit=document.createElementNS('http://www.w3.org/2000/svg','path');hit.setAttribute('d',curve(e));hit.classList.add('edge-hit');hit.onclick=ev=>{ev.stopPropagation();show(e)};edgeLayer.appendChild(hit);const a=anchor(e.source,true),b=anchor(e.target,false),label=document.createElementNS('http://www.w3.org/2000/svg','text');label.setAttribute('x',(a.x+b.x)/2);label.setAttribute('y',(a.y+b.y)/2-6);label.setAttribute('text-anchor','middle');label.classList.add('label');label.textContent=e.count>1?`${e.relation} ×${e.count}`:e.relation;labelLayer.appendChild(label)}for(const n of graph.nodes){const p=positions.get(n.id);if(!p)continue;const group=document.createElementNS('http://www.w3.org/2000/svg','g');group.classList.add('node');group.dataset.id=n.id;group.dataset.search=`${n.id} ${n.name||''} ${n.type||''}`.toLowerCase();group.setAttribute('transform',`translate(${p.x} ${p.y})`);const rect=document.createElementNS('http://www.w3.org/2000/svg','rect');rect.setAttribute('width',160);rect.setAttribute('height',50);rect.setAttribute('fill',color(n.type));group.appendChild(rect);const type=document.createElementNS('http://www.w3.org/2000/svg','text');type.setAttribute('x',10);type.setAttribute('y',15);type.classList.add('type');type.textContent=n.type||'unknown';group.appendChild(type);const label=document.createElementNS('http://www.w3.org/2000/svg','text');label.setAttribute('x',10);label.setAttribute('y',34);label.textContent=short(n);group.appendChild(label);group.onclick=ev=>{ev.stopPropagation();show(n)};nodeLayer.appendChild(group)}applySearch()}
59
+ function applyTransform(){viewport.setAttribute('transform',`translate(${transform.x} ${transform.y}) scale(${transform.scale})`)}
60
+ function fit(){if(!positions.size)return;const xs=[...positions.values()].map(p=>p.x),ys=[...positions.values()].map(p=>p.y),minX=Math.min(...xs),maxX=Math.max(...xs)+160,minY=Math.min(...ys),maxY=Math.max(...ys)+50,box=svg.getBoundingClientRect(),w=Math.max(1,maxX-minX),h=Math.max(1,maxY-minY),scale=Math.min(1.2,Math.max(.07,Math.min((box.width-60)/w,(box.height-60)/h)));transform={x:30-minX*scale,y:30-minY*scale,scale};applyTransform()}
61
+ function applySearch(){const q=search.value.trim().toLowerCase();const matched=new Set();document.querySelectorAll('.node').forEach(el=>{const ok=!q||el.dataset.search.includes(q);el.classList.toggle('dim',!ok);if(ok)matched.add(el.dataset.id)});document.querySelectorAll('.edge').forEach(el=>{const keep=!q||matched.has(el.dataset.source)||matched.has(el.dataset.target)||el.dataset.relation.includes(q);el.classList.toggle('dim',!keep)})}
62
+ async function poll(){try{const response=await fetch('/graph.json',{cache:'no-store'});if(!response.ok)throw new Error(String(response.status));const data=await response.json(),finished=!!data.live_finished;delete data.live_finished;const signature=`${data.node_count||0}:${data.edge_count||0}:${data.event_count||0}`;graph=data;stats.textContent=`${data.node_count||0} nodes · ${data.edge_count||0} edges · ${data.event_count||0} events`;status.textContent=finished?'FINISHED':'LIVE';if(signature!==lastSignature){lastSignature=signature;render();if(positions.size<4)fit()}if(finished){setTimeout(()=>{location.href='/final'},250);return}}catch(_){status.textContent='RECONNECTING'}setTimeout(poll,500)}
63
+ svg.onpointerdown=e=>{if(e.target.closest?.('.node'))return;pan={x:e.clientX,y:e.clientY,tx:transform.x,ty:transform.y};svg.classList.add('panning');svg.setPointerCapture(e.pointerId)};svg.onpointermove=e=>{if(!pan)return;transform.x=pan.tx+e.clientX-pan.x;transform.y=pan.ty+e.clientY-pan.y;applyTransform()};svg.onpointerup=e=>{pan=null;svg.classList.remove('panning');try{svg.releasePointerCapture(e.pointerId)}catch(_){}};svg.addEventListener('wheel',e=>{e.preventDefault();const rect=svg.getBoundingClientRect(),mx=e.clientX-rect.left,my=e.clientY-rect.top,old=transform.scale,next=Math.min(4,Math.max(.07,old*Math.exp(-e.deltaY*.0012))),gx=(mx-transform.x)/old,gy=(my-transform.y)/old;transform.scale=next;transform.x=mx-gx*next;transform.y=my-gy*next;applyTransform()},{passive:false});search.oninput=applySearch;document.getElementById('fit').onclick=fit;window.onresize=fit;applyTransform();poll();
64
+ })();
65
+ </script>
66
+ </body>
67
+ </html>"""
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class LiveResult:
72
+ session_id: str
73
+ return_code: int
74
+ live_url: str
75
+ output_dir: Path
76
+ event_stream: Path
77
+ graph: Path
78
+ viewer: Path
79
+
80
+ def to_dict(self) -> dict[str, object]:
81
+ return {
82
+ "session_id": self.session_id,
83
+ "return_code": self.return_code,
84
+ "live_url": self.live_url,
85
+ "output_dir": str(self.output_dir),
86
+ "event_stream": str(self.event_stream),
87
+ "graph": str(self.graph),
88
+ "viewer": str(self.viewer),
89
+ }
90
+
91
+
92
+ class _LiveState:
93
+ def __init__(self, session_id: str, event_path: Path) -> None:
94
+ self.session_id = session_id
95
+ self.event_path = event_path
96
+ self._lock = threading.Lock()
97
+ self._last_graph: dict[str, object] = self._empty_graph()
98
+ self._finished = False
99
+ self._final_html: str | None = None
100
+
101
+ def _empty_graph(self) -> dict[str, object]:
102
+ return {
103
+ "graph_schema_version": "0.1",
104
+ "session_id": self.session_id,
105
+ "event_count": 0,
106
+ "node_count": 0,
107
+ "edge_count": 0,
108
+ "nodes": [],
109
+ "edges": [],
110
+ }
111
+
112
+ def snapshot(self) -> dict[str, object]:
113
+ if self.event_path.exists() and self.event_path.stat().st_size > 0:
114
+ try:
115
+ graph = build_execution_graph(self.event_path, allow_incomplete=True).to_dict()
116
+ except (OSError, ValueError, json.JSONDecodeError):
117
+ graph = None
118
+ if graph is not None:
119
+ with self._lock:
120
+ self._last_graph = graph
121
+ with self._lock:
122
+ payload = dict(self._last_graph)
123
+ payload["live_finished"] = self._finished
124
+ return payload
125
+
126
+ def finish(self, graph: dict[str, object]) -> None:
127
+ with self._lock:
128
+ self._last_graph = graph
129
+ self._final_html = render_graph_html(graph)
130
+ self._finished = True
131
+
132
+ def final_html(self) -> str | None:
133
+ with self._lock:
134
+ return self._final_html
135
+
136
+
137
+ def _handler_factory(state: _LiveState):
138
+ class Handler(BaseHTTPRequestHandler):
139
+ def log_message(self, format: str, *args: object) -> None:
140
+ return
141
+
142
+ def _send(self, body: bytes, content_type: str, status: int = 200) -> None:
143
+ self.send_response(status)
144
+ self.send_header("Content-Type", content_type)
145
+ self.send_header("Content-Length", str(len(body)))
146
+ self.send_header("Cache-Control", "no-store")
147
+ self.send_header("X-Content-Type-Options", "nosniff")
148
+ self.end_headers()
149
+ self.wfile.write(body)
150
+
151
+ def do_GET(self) -> None:
152
+ path = self.path.split("?", 1)[0]
153
+ if path == "/":
154
+ self._send(_LIVE_HTML.encode("utf-8"), "text/html; charset=utf-8")
155
+ return
156
+ if path == "/graph.json":
157
+ payload = json.dumps(
158
+ state.snapshot(), ensure_ascii=False, separators=(",", ":")
159
+ ).encode("utf-8")
160
+ self._send(payload, "application/json; charset=utf-8")
161
+ return
162
+ if path == "/final":
163
+ final = state.final_html()
164
+ if final is None:
165
+ self._send(b"Final graph is not ready", "text/plain; charset=utf-8", 404)
166
+ else:
167
+ self._send(final.encode("utf-8"), "text/html; charset=utf-8")
168
+ return
169
+ self._send(b"Not found", "text/plain; charset=utf-8", 404)
170
+
171
+ return Handler
172
+
173
+
174
+ class _LocalThreadingHTTPServer(ThreadingHTTPServer):
175
+ """Local HTTP server that skips HTTPServer's reverse-DNS lookup during bind."""
176
+
177
+ def server_bind(self) -> None:
178
+ TCPServer.server_bind(self)
179
+ host, port = self.server_address[:2]
180
+ self.server_name = str(host)
181
+ self.server_port = int(port)
182
+
183
+
184
+ def run_live(
185
+ command: list[str],
186
+ *,
187
+ watch_root: str | Path,
188
+ output_dir: str | Path | None = None,
189
+ poll_interval: float = 0.10,
190
+ collect_filesystem: bool = True,
191
+ collect_network: bool = True,
192
+ port: int = 0,
193
+ open_browser: bool = True,
194
+ linger_seconds: float = 2.0,
195
+ announce: Callable[[str], None] | None = None,
196
+ ) -> LiveResult:
197
+ """Run a command with the portable collector and expose a localhost live graph."""
198
+ if not command:
199
+ raise ValueError("command must not be empty")
200
+ if port < 0 or port > 65535:
201
+ raise ValueError("port must be between 0 and 65535")
202
+ if linger_seconds < 0:
203
+ raise ValueError("linger_seconds must be >= 0")
204
+
205
+ session_id = uuid4().hex
206
+ root = Path(watch_root).expanduser().resolve()
207
+ run_dir = (
208
+ Path(output_dir).expanduser().resolve()
209
+ if output_dir is not None
210
+ else root / ".execweave" / "runs" / session_id
211
+ )
212
+ run_dir.mkdir(parents=True, exist_ok=True)
213
+ event_path = run_dir / "events.jsonl"
214
+ graph_path = run_dir / "graph.json"
215
+ viewer_path = run_dir / "viewer.html"
216
+ for artifact in (event_path, graph_path, viewer_path):
217
+ if artifact.exists() and artifact.stat().st_size > 0:
218
+ raise FileExistsError(f"ExecWeave live artifact already exists: {artifact}")
219
+
220
+ sink = JsonlSink(event_path)
221
+ collector = create_collector(
222
+ backend="portable",
223
+ session_id=session_id,
224
+ sink=sink,
225
+ watch_root=root,
226
+ poll_interval=poll_interval,
227
+ collect_filesystem=collect_filesystem,
228
+ collect_network=collect_network,
229
+ )
230
+
231
+ state = _LiveState(session_id, event_path)
232
+ server = _LocalThreadingHTTPServer(("127.0.0.1", port), _handler_factory(state))
233
+ server.daemon_threads = True
234
+ server_thread = threading.Thread(
235
+ target=server.serve_forever,
236
+ name="execweave-live",
237
+ daemon=True,
238
+ )
239
+ server_thread.start()
240
+ host, selected_port = server.server_address[:2]
241
+ live_url = f"http://{host}:{selected_port}/"
242
+ if announce is not None:
243
+ announce(live_url)
244
+ if open_browser:
245
+ webbrowser.open(live_url)
246
+
247
+ return_code = 1
248
+ try:
249
+ return_code = collector.run(command)
250
+ validation = validate_event_stream(event_path)
251
+ if not validation.valid:
252
+ details = "; ".join(validation.errors)
253
+ raise RuntimeError(f"live event stream failed validation: {details}")
254
+
255
+ execution_graph = build_execution_graph(event_path)
256
+ graph_payload = execution_graph.to_dict()
257
+ write_execution_graph(execution_graph, graph_path)
258
+ write_graph_html(graph_payload, viewer_path, open_browser=False)
259
+ state.finish(graph_payload)
260
+ if linger_seconds:
261
+ time.sleep(linger_seconds)
262
+
263
+ return LiveResult(
264
+ session_id=session_id,
265
+ return_code=return_code,
266
+ live_url=live_url,
267
+ output_dir=run_dir,
268
+ event_stream=event_path,
269
+ graph=graph_path,
270
+ viewer=viewer_path,
271
+ )
272
+ finally:
273
+ server.shutdown()
274
+ server.server_close()
275
+ server_thread.join(timeout=5)