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/graph.py ADDED
@@ -0,0 +1,300 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import asdict, dataclass, field
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .validate import validate_event_stream
10
+
11
+ GRAPH_SCHEMA_VERSION = "0.1"
12
+
13
+
14
+ @dataclass
15
+ class GraphNode:
16
+ id: str
17
+ type: str
18
+ name: str | None = None
19
+ attributes: dict[str, Any] = field(default_factory=dict)
20
+ first_seen: str | None = None
21
+ last_seen: str | None = None
22
+ event_count: int = 0
23
+ event_types: set[str] = field(default_factory=set)
24
+
25
+ def observe(self, entity: dict[str, Any], event: dict[str, Any]) -> None:
26
+ self.name = self.name or entity.get("name")
27
+ incoming = entity.get("attributes") or {}
28
+ if isinstance(incoming, dict):
29
+ for key, value in incoming.items():
30
+ self.attributes.setdefault(key, value)
31
+ timestamp = event.get("timestamp")
32
+ if isinstance(timestamp, str):
33
+ self.first_seen = min(self.first_seen, timestamp) if self.first_seen else timestamp
34
+ self.last_seen = max(self.last_seen, timestamp) if self.last_seen else timestamp
35
+ self.event_count += 1
36
+ event_type = event.get("event_type")
37
+ if isinstance(event_type, str):
38
+ self.event_types.add(event_type)
39
+
40
+ def to_dict(self) -> dict[str, Any]:
41
+ payload = asdict(self)
42
+ payload["event_types"] = sorted(self.event_types)
43
+ return payload
44
+
45
+
46
+ @dataclass
47
+ class GraphEdge:
48
+ id: str
49
+ source: str
50
+ target: str
51
+ relation: str
52
+ count: int = 0
53
+ first_seen: str | None = None
54
+ last_seen: str | None = None
55
+ first_sequence: int | None = None
56
+ last_sequence: int | None = None
57
+ event_ids: list[str] = field(default_factory=list)
58
+ event_types: set[str] = field(default_factory=set)
59
+ backends: set[str] = field(default_factory=set)
60
+ attributions: set[str] = field(default_factory=set)
61
+ causal_values: set[bool] = field(default_factory=set)
62
+ inferred_values: set[bool] = field(default_factory=set)
63
+ inference_methods: set[str] = field(default_factory=set)
64
+ identity_exact_values: set[bool] = field(default_factory=set)
65
+ identity_methods: set[str] = field(default_factory=set)
66
+ identity_hashes: set[str] = field(default_factory=set)
67
+ confidence_values: list[float] = field(default_factory=list)
68
+ confidence_semantics: set[str] = field(default_factory=set)
69
+ supporting_event_ids: set[str] = field(default_factory=set)
70
+
71
+ def observe(self, event: dict[str, Any]) -> None:
72
+ self.count += 1
73
+ timestamp = event.get("timestamp")
74
+ if isinstance(timestamp, str):
75
+ self.first_seen = min(self.first_seen, timestamp) if self.first_seen else timestamp
76
+ self.last_seen = max(self.last_seen, timestamp) if self.last_seen else timestamp
77
+ sequence = event.get("sequence")
78
+ if isinstance(sequence, int) and not isinstance(sequence, bool):
79
+ self.first_sequence = (
80
+ min(self.first_sequence, sequence) if self.first_sequence is not None else sequence
81
+ )
82
+ self.last_sequence = (
83
+ max(self.last_sequence, sequence) if self.last_sequence is not None else sequence
84
+ )
85
+ event_id = event.get("event_id")
86
+ if isinstance(event_id, str):
87
+ self.event_ids.append(event_id)
88
+ event_type = event.get("event_type")
89
+ if isinstance(event_type, str):
90
+ self.event_types.add(event_type)
91
+ attributes = event.get("attributes") or {}
92
+ if isinstance(attributes, dict):
93
+ backend = attributes.get("backend")
94
+ if isinstance(backend, str):
95
+ self.backends.add(backend)
96
+ attribution = attributes.get("attribution")
97
+ if isinstance(attribution, str):
98
+ self.attributions.add(attribution)
99
+ causal = attributes.get("causal")
100
+ if isinstance(causal, bool):
101
+ self.causal_values.add(causal)
102
+ inferred = attributes.get("inferred")
103
+ if isinstance(inferred, bool):
104
+ self.inferred_values.add(inferred)
105
+ method = attributes.get("inference_method")
106
+ if isinstance(method, str) and method:
107
+ self.inference_methods.add(method)
108
+ identity_exact = attributes.get("identity_exact")
109
+ if isinstance(identity_exact, bool):
110
+ self.identity_exact_values.add(identity_exact)
111
+ identity_method = attributes.get("identity_method")
112
+ if isinstance(identity_method, str) and identity_method:
113
+ self.identity_methods.add(identity_method)
114
+ identity_hash = attributes.get("shared_request_id_hash")
115
+ if isinstance(identity_hash, str) and identity_hash:
116
+ self.identity_hashes.add(identity_hash)
117
+ confidence = attributes.get("confidence")
118
+ if isinstance(confidence, (int, float)) and not isinstance(confidence, bool):
119
+ self.confidence_values.append(float(confidence))
120
+ semantics = attributes.get("confidence_semantics")
121
+ if isinstance(semantics, str) and semantics:
122
+ self.confidence_semantics.add(semantics)
123
+ supporting = attributes.get("supporting_event_ids")
124
+ if isinstance(supporting, list):
125
+ self.supporting_event_ids.update(
126
+ value for value in supporting if isinstance(value, str) and value
127
+ )
128
+
129
+ def to_dict(self) -> dict[str, Any]:
130
+ if self.causal_values == {True}:
131
+ causal: bool | None = True
132
+ elif self.causal_values == {False}:
133
+ causal = False
134
+ else:
135
+ causal = None
136
+ if self.inferred_values == {True}:
137
+ inferred: bool | None = True
138
+ elif self.inferred_values == {False}:
139
+ inferred = False
140
+ else:
141
+ inferred = None
142
+ if self.identity_exact_values == {True}:
143
+ identity_exact: bool | None = True
144
+ elif self.identity_exact_values == {False}:
145
+ identity_exact = False
146
+ else:
147
+ identity_exact = None
148
+ return {
149
+ "id": self.id,
150
+ "source": self.source,
151
+ "target": self.target,
152
+ "relation": self.relation,
153
+ "count": self.count,
154
+ "first_seen": self.first_seen,
155
+ "last_seen": self.last_seen,
156
+ "first_sequence": self.first_sequence,
157
+ "last_sequence": self.last_sequence,
158
+ "event_ids": self.event_ids,
159
+ "event_types": sorted(self.event_types),
160
+ "backends": sorted(self.backends),
161
+ "attributions": sorted(self.attributions),
162
+ "causal": causal,
163
+ "inferred": inferred,
164
+ "inference_methods": sorted(self.inference_methods),
165
+ "identity_exact": identity_exact,
166
+ "identity_methods": sorted(self.identity_methods),
167
+ "identity_hashes": sorted(self.identity_hashes),
168
+ "confidence_min": min(self.confidence_values) if self.confidence_values else None,
169
+ "confidence_max": max(self.confidence_values) if self.confidence_values else None,
170
+ "confidence_semantics": sorted(self.confidence_semantics),
171
+ "supporting_event_ids": sorted(self.supporting_event_ids),
172
+ }
173
+
174
+
175
+ @dataclass
176
+ class ExecutionGraph:
177
+ session_id: str
178
+ source_path: str
179
+ source_schema_versions: list[str]
180
+ event_count: int
181
+ nodes: list[GraphNode]
182
+ edges: list[GraphEdge]
183
+ built_at: str = field(
184
+ default_factory=lambda: datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
185
+ )
186
+
187
+ def to_dict(self) -> dict[str, Any]:
188
+ return {
189
+ "graph_schema_version": GRAPH_SCHEMA_VERSION,
190
+ "session_id": self.session_id,
191
+ "source_path": self.source_path,
192
+ "source_schema_versions": self.source_schema_versions,
193
+ "event_count": self.event_count,
194
+ "node_count": len(self.nodes),
195
+ "edge_count": len(self.edges),
196
+ "built_at": self.built_at,
197
+ "nodes": [node.to_dict() for node in self.nodes],
198
+ "edges": [edge.to_dict() for edge in self.edges],
199
+ }
200
+
201
+
202
+ def _edge_id(source: str, relation: str, target: str) -> str:
203
+ return f"{source}--{relation}-->{target}"
204
+
205
+
206
+ def _load_events(path: Path) -> list[dict[str, Any]]:
207
+ return [
208
+ json.loads(line)
209
+ for line in path.read_text(encoding="utf-8").splitlines()
210
+ if line.strip()
211
+ ]
212
+
213
+
214
+ def build_execution_graph(
215
+ path: str | Path,
216
+ *,
217
+ allow_incomplete: bool = False,
218
+ ) -> ExecutionGraph:
219
+ source_path = Path(path).expanduser().resolve()
220
+ validation = validate_event_stream(
221
+ source_path,
222
+ require_complete_session=not allow_incomplete,
223
+ )
224
+ if not validation.valid:
225
+ details = "; ".join(validation.errors)
226
+ raise ValueError(f"invalid ExecWeave event stream: {details}")
227
+
228
+ events = _load_events(source_path)
229
+ nodes: dict[str, GraphNode] = {}
230
+ edges: dict[tuple[str, str, str], GraphEdge] = {}
231
+
232
+ for event in events:
233
+ source = event.get("source")
234
+ target = event.get("target")
235
+
236
+ for entity in (source, target):
237
+ if not isinstance(entity, dict):
238
+ continue
239
+ entity_id = entity.get("id")
240
+ entity_type = entity.get("type")
241
+ if not isinstance(entity_id, str) or not isinstance(entity_type, str):
242
+ continue
243
+ node = nodes.get(entity_id)
244
+ if node is None:
245
+ node = GraphNode(
246
+ id=entity_id,
247
+ type=entity_type,
248
+ name=entity.get("name") if isinstance(entity.get("name"), str) else None,
249
+ )
250
+ nodes[entity_id] = node
251
+ node.observe(entity, event)
252
+
253
+ if not isinstance(source, dict) or not isinstance(target, dict):
254
+ continue
255
+ source_id = source.get("id")
256
+ target_id = target.get("id")
257
+ relation = event.get("relation")
258
+ if not all(isinstance(value, str) and value for value in (source_id, target_id, relation)):
259
+ continue
260
+ key = (source_id, relation, target_id)
261
+ edge = edges.get(key)
262
+ if edge is None:
263
+ edge = GraphEdge(
264
+ id=_edge_id(source_id, relation, target_id),
265
+ source=source_id,
266
+ target=target_id,
267
+ relation=relation,
268
+ )
269
+ edges[key] = edge
270
+ edge.observe(event)
271
+
272
+ session_id = validation.session_ids[0] if validation.session_ids else "unknown"
273
+ return ExecutionGraph(
274
+ session_id=session_id,
275
+ source_path=str(source_path),
276
+ source_schema_versions=validation.schema_versions,
277
+ event_count=len(events),
278
+ nodes=sorted(nodes.values(), key=lambda node: (node.type, node.id)),
279
+ edges=sorted(edges.values(), key=lambda edge: (edge.source, edge.relation, edge.target)),
280
+ )
281
+
282
+
283
+ def write_execution_graph(
284
+ graph: ExecutionGraph,
285
+ path: str | Path,
286
+ *,
287
+ metadata: dict[str, Any] | None = None,
288
+ ) -> Path:
289
+ output = Path(path).expanduser().resolve()
290
+ output.parent.mkdir(parents=True, exist_ok=True)
291
+ if output.exists() and output.stat().st_size > 0:
292
+ raise FileExistsError(f"ExecWeave graph output already exists: {output}")
293
+ payload = graph.to_dict()
294
+ if metadata is not None:
295
+ payload["metadata"] = metadata
296
+ output.write_text(
297
+ json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
298
+ encoding="utf-8",
299
+ )
300
+ return output