pgs-runtime 0.3.1__tar.gz → 0.4.0__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.
Files changed (33) hide show
  1. {pgs_runtime-0.3.1/pgs_runtime.egg-info → pgs_runtime-0.4.0}/PKG-INFO +1 -1
  2. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime/cli.py +73 -3
  3. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime/dispatcher.py +1 -3
  4. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime/evidence.py +3 -3
  5. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime/scheduler.py +3 -3
  6. pgs_runtime-0.4.0/pgs_runtime/trace_viz.py +264 -0
  7. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0/pgs_runtime.egg-info}/PKG-INFO +1 -1
  8. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime.egg-info/SOURCES.txt +1 -0
  9. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pyproject.toml +1 -1
  10. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/LICENSE +0 -0
  11. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/MANIFEST.in +0 -0
  12. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/NOTICE +0 -0
  13. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/README.md +0 -0
  14. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime/__init__.py +0 -0
  15. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime/conformance.py +0 -0
  16. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime/ct_errors.py +0 -0
  17. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime/ct_execute.py +0 -0
  18. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime/ct_executor.py +0 -0
  19. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime/examine/__init__.py +0 -0
  20. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime/examine/classifier.py +0 -0
  21. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime/examine/cli.py +0 -0
  22. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime/examine/conformance_test.py +0 -0
  23. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime/examine/hint_engine.py +0 -0
  24. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime/examine/locator.py +0 -0
  25. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime/examine/parser.py +0 -0
  26. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime/examine/reporter.py +0 -0
  27. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime/loader.py +0 -0
  28. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime/memory.py +0 -0
  29. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime/server.py +0 -0
  30. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime.egg-info/dependency_links.txt +0 -0
  31. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime.egg-info/entry_points.txt +0 -0
  32. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/pgs_runtime.egg-info/top_level.txt +0 -0
  33. {pgs_runtime-0.3.1 → pgs_runtime-0.4.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgs_runtime
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: PGS runtime for Protocol-Governed Systems
5
5
  Author: Bachi
6
6
  License-Expression: Apache-2.0
@@ -2,16 +2,18 @@
2
2
  cli.py — Token-native CLI entry point for the pgs_runtime.
3
3
 
4
4
  Commands:
5
- run — Execute a workflow against the tokenized snapshot.
6
- examine — Analyze a completed trace file and print a diagnostic report.
5
+ run — Execute a workflow against the tokenized snapshot.
6
+ examine — Analyze a completed trace file and print a diagnostic report.
7
+ visualize — Render execution-path PNG from a completed trace file.
7
8
 
8
9
  Execution path (run):
9
10
  1. Load tokenized snapshot for the domain via loader.load_domain()
10
11
  — verifies topology hash against trust attestation; fails hard on mismatch
11
12
  2. Generate deterministic trace ID from (domain, wf_fqdn, payload)
12
- 3. Open TraceWriter at traces/<domain>/<trace_id>/
13
+ 3. Open TraceWriter at traces/<domain>/<wf_code>/<trace_id>/
13
14
  4. Drive workflow topology via scheduler.run_wf()
14
15
  5. Print result summary; exit 1 on non-SUCCESS
16
+ 6. If --visualize: invoke evidence projection (trace_viz) to render PNG
15
17
 
16
18
  All runtime behavior comes from the compiled tokenized_snapshot.
17
19
  The CLI does not implement any domain logic.
@@ -69,6 +71,13 @@ def _build_parser() -> argparse.ArgumentParser:
69
71
  "tokenized_snapshot/ and traces/ live here",
70
72
  )
71
73
 
74
+ # ── run: optional visualization flag ─────────────────────────
75
+ run_p.add_argument(
76
+ "--visualize",
77
+ action="store_true",
78
+ help="Render execution-path PNG after run (requires graphviz)",
79
+ )
80
+
72
81
  # ── examine ───────────────────────────────────────────────────
73
82
  ex_p = subs.add_parser("examine", help="Analyze a completed trace file")
74
83
  ex_p.add_argument(
@@ -77,6 +86,22 @@ def _build_parser() -> argparse.ArgumentParser:
77
86
  help="Path to a completed .jsonl trace file",
78
87
  )
79
88
 
89
+ # ── visualize ─────────────────────────────────────────────────
90
+ viz_p = subs.add_parser(
91
+ "visualize",
92
+ help="Render execution-path PNG from a completed trace file",
93
+ )
94
+ viz_p.add_argument(
95
+ "trace_file",
96
+ metavar="FILE",
97
+ help="Path to a completed .jsonl trace file",
98
+ )
99
+ viz_p.add_argument(
100
+ "--workspace",
101
+ metavar="PATH",
102
+ help="Absolute path to pgs_workspace root (or set PGS_WORKSPACE)",
103
+ )
104
+
80
105
  return parser
81
106
 
82
107
 
@@ -161,6 +186,12 @@ def _handle_run(args: argparse.Namespace) -> None:
161
186
 
162
187
  duration_ms = int((time.monotonic() - t0) * 1000)
163
188
 
189
+ # Evidence projection: render execution-path PNG if requested
190
+ trace_path = trace_dir / f"{trace_id}.jsonl"
191
+ png_path = None
192
+ if args.visualize:
193
+ png_path = _render_visualization(workspace, trace_path)
194
+
164
195
  print("=" * 60)
165
196
  print("[pgs_runtime] Workflow Complete")
166
197
  print("=" * 60)
@@ -170,12 +201,39 @@ def _handle_run(args: argparse.Namespace) -> None:
170
201
  print(f"Duration: {duration_ms}ms")
171
202
  if surface:
172
203
  print(f"Output: {json.dumps(surface, separators=(',', ':'))}")
204
+ if png_path:
205
+ print(f"Visual: {png_path}")
206
+ elif args.visualize:
207
+ print("Visual: (graphviz not available — PNG skipped)")
173
208
  print("=" * 60)
174
209
 
175
210
  if result_status not in ("SUCCESS", "ALREADY_EXISTS"):
176
211
  sys.exit(1)
177
212
 
178
213
 
214
+ def _handle_visualize(args: argparse.Namespace) -> None:
215
+ trace_path = Path(args.trace_file)
216
+ if not trace_path.exists():
217
+ _fatal(f"Trace file not found: {args.trace_file}")
218
+
219
+ workspace_str = args.workspace or os.environ.get("PGS_WORKSPACE")
220
+ if not workspace_str:
221
+ _fatal("--workspace PATH or PGS_WORKSPACE is required")
222
+ workspace = Path(workspace_str)
223
+ if not workspace.is_absolute():
224
+ _fatal(f"--workspace must be an absolute path, got: {workspace_str}")
225
+
226
+ png_path = _render_visualization(workspace, trace_path)
227
+ if png_path:
228
+ print(f"[pgs_runtime] Execution path PNG: {png_path}")
229
+ else:
230
+ print(
231
+ "[pgs_runtime] Visualization skipped — graphviz (dot) not available.",
232
+ file=sys.stderr,
233
+ )
234
+ sys.exit(1)
235
+
236
+
179
237
  def _handle_examine(args: argparse.Namespace) -> None:
180
238
  trace_path = Path(args.trace_file)
181
239
  if not trace_path.exists():
@@ -205,6 +263,16 @@ def _handle_examine(args: argparse.Namespace) -> None:
205
263
  # Utilities
206
264
  # ---------------------------------------------------------------------------
207
265
 
266
+ def _render_visualization(workspace: Path, trace_path: Path) -> "Path | None":
267
+ """Invoke evidence projection to render execution-path PNG. Best-effort."""
268
+ from pgs_runtime.trace_viz import render_trace_png
269
+ try:
270
+ return render_trace_png(workspace, trace_path)
271
+ except (FileNotFoundError, ValueError) as exc:
272
+ print(f"[pgs_runtime] Visualization error: {exc}", file=sys.stderr)
273
+ return None
274
+
275
+
208
276
  def _load_payload(payload_path: str | None) -> dict:
209
277
  if not payload_path:
210
278
  return {}
@@ -234,6 +302,8 @@ def main() -> None:
234
302
  _handle_run(args)
235
303
  elif args.command == "examine":
236
304
  _handle_examine(args)
305
+ elif args.command == "visualize":
306
+ _handle_visualize(args)
237
307
 
238
308
 
239
309
  if __name__ == "__main__":
@@ -202,9 +202,7 @@ def _execute_cs_step(
202
202
  # Resolve per-RB policy config for this CS
203
203
  rb_cs_map = pkg.handlers.rb_policy.get(rb_addr, {})
204
204
  policy_entry = rb_cs_map.get(cs_addr, {})
205
- # Compiler emits either "policy" or "config" as the key name depending on
206
- # the structure; accept both to remain forward/backward compatible.
207
- policy_raw = policy_entry.get("policy") or policy_entry.get("config") or {}
205
+ policy_raw = policy_entry.get("policy") or {}
208
206
  policy = _expand_policy(policy_raw, data_root)
209
207
 
210
208
  # Instantiate CS runtime
@@ -15,12 +15,12 @@ Each execution event is a self-contained JSON line with:
15
15
  - ts_ns — monotonic nanosecond timestamp
16
16
 
17
17
  The trace file is written to:
18
- <traces_root>/<domain>/<subdomain>/<trace_id>/<trace_id>.jsonl
18
+ <traces_root>/<domain>/<wf_code>/<trace_id>/<trace_id>.jsonl
19
19
 
20
20
  subdomain is not known to this module — caller passes the full trace dir path.
21
21
 
22
- Projection (human-readable reports, visualizations) belongs in tooling/compiler,
23
- not here. The runtime produces .jsonl only.
22
+ This module produces the raw JSONL evidence only. Evidence projection
23
+ (execution-path PNG overlay) is handled separately in trace_viz.py.
24
24
  """
25
25
 
26
26
  from __future__ import annotations
@@ -20,8 +20,8 @@ Topology traversal rules:
20
20
 
21
21
  Boundary nodes (IN_, EXIT_):
22
22
  Nodes without a pipeline entry (not in dispatch.pipeline) are boundary nodes.
23
- IN_ nodes perform admission gating; for v0.3.0, prior to admission_snapshot
24
- integration, they pass through as ACK. The routing table routes ACK forward.
23
+ IN_ nodes perform admission gating; prior to admission_snapshot integration,
24
+ they pass through as ACK. The routing table routes ACK forward.
25
25
  EXIT_ nodes (no routing) terminate the loop naturally.
26
26
 
27
27
  Bindings path grammar (WF-level, compiler-emitted):
@@ -122,7 +122,7 @@ def run_wf(
122
122
 
123
123
  else:
124
124
  # Boundary node (IN_, EXIT_) — no pipeline
125
- # v0.3.0: admission_snapshot not yet integrated; IN_ nodes pass as ACK
125
+ # admission_snapshot not yet integrated; IN_ nodes pass as ACK
126
126
  result_status = "ACK"
127
127
 
128
128
  # Resolve result_status → condition address and route to next node
@@ -0,0 +1,264 @@
1
+ """
2
+ trace_viz.py — Evidence projection: execution path visualization.
3
+
4
+ Reads a completed trace (.jsonl) and the compiled workflow graph (.graph.json)
5
+ from the protocol snapshot visualization directory, then generates a PNG with
6
+ the actual execution path overlaid in red on the static compiled graph.
7
+
8
+ This is Evidence Projection — not a runtime execution feature:
9
+
10
+ topology (graph.json)
11
+ +
12
+ evidence (trace.jsonl)
13
+ ───────────────────────
14
+ → execution path PNG
15
+
16
+ Inputs (both already materialized, read-only):
17
+ protocol_snapshot/visualization/<WF_CODE>/<WF_CODE>.graph.json
18
+ traces/<domain>/<wf_code>/<trace_id>/<trace_id>.jsonl
19
+
20
+ Output:
21
+ traces/<domain>/<wf_code>/<trace_id>/<trace_id>.png
22
+
23
+ Architectural invariant:
24
+ This module reads ONLY from:
25
+ - protocol_snapshot/visualization/ (compiled graph artifacts)
26
+ - the caller-supplied trace .jsonl (execution evidence)
27
+ It does NOT walk protocol_snapshot/artifacts/ or any other canonical
28
+ protocol location. Visualization is overlay-only — no protocol interpretation.
29
+
30
+ Uses graphviz (dot) — returns None silently if dot is not available.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import json
36
+ import subprocess
37
+ from pathlib import Path
38
+ from typing import Optional
39
+
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Public API
43
+ # ---------------------------------------------------------------------------
44
+
45
+ def render_trace_png(
46
+ workspace: Path,
47
+ trace_path: Path,
48
+ ) -> Optional[Path]:
49
+ """
50
+ Generate execution-path overlay PNG for a completed trace.
51
+
52
+ Reads CC_COMPLETE events from trace_path to reconstruct the actual
53
+ execution path, then overlays it (red) on the compiled workflow graph.
54
+
55
+ Args:
56
+ workspace: Absolute path to pgs_workspace root.
57
+ trace_path: Path to the completed .jsonl trace file.
58
+
59
+ Returns:
60
+ Path to the generated PNG, or None if graphviz is unavailable.
61
+
62
+ Raises:
63
+ FileNotFoundError: trace_path or graph.json does not exist.
64
+ ValueError: Trace is empty or missing WF_START event.
65
+ """
66
+ if not trace_path.exists():
67
+ raise FileNotFoundError(f"Trace file not found: {trace_path}")
68
+
69
+ # Parse trace events
70
+ events: list[dict] = []
71
+ for line in trace_path.read_text(encoding="utf-8").splitlines():
72
+ line = line.strip()
73
+ if line:
74
+ events.append(json.loads(line))
75
+
76
+ if not events:
77
+ raise ValueError(f"Trace file is empty: {trace_path}")
78
+
79
+ # Extract WF code from WF_START event
80
+ wf_start = next((e for e in events if e["event_type"] == "WF_START"), None)
81
+ if wf_start is None:
82
+ raise ValueError(f"No WF_START event found in: {trace_path}")
83
+
84
+ wf_fqdn = wf_start["detail"]["wf_fqdn"]
85
+ wf_code = wf_fqdn.split("::")[-1] # e.g. "WF_REGISTER_ACTOR_UNVERIFIED_V0"
86
+
87
+ # Load compiled graph from protocol_snapshot/visualization/
88
+ graph_path = (
89
+ workspace
90
+ / "protocol_snapshot"
91
+ / "visualization"
92
+ / wf_code
93
+ / f"{wf_code}.graph.json"
94
+ )
95
+ if not graph_path.exists():
96
+ raise FileNotFoundError(
97
+ f"Compiled graph not found: {graph_path}\n"
98
+ f"Re-run the compiler to regenerate visualization artifacts."
99
+ )
100
+
101
+ graph = json.loads(graph_path.read_text(encoding="utf-8"))
102
+
103
+ # Reconstruct actual execution path from trace events + graph edges
104
+ path = _extract_execution_path(events, graph)
105
+
106
+ # Build visited node and taken edge sets
107
+ visited_nodes: set[str] = set()
108
+ taken_edges: set[tuple[str, str, str]] = set() # (from_node, to_node, condition)
109
+ for from_node, condition, to_node in path:
110
+ visited_nodes.add(from_node)
111
+ visited_nodes.add(to_node)
112
+ taken_edges.add((from_node, to_node, condition))
113
+
114
+ # Generate DOT source with execution-path overlay
115
+ dot_content = _generate_dot(graph, visited_nodes, taken_edges)
116
+
117
+ # Render: write DOT, invoke graphviz, clean up DOT
118
+ png_path = trace_path.with_suffix(".png")
119
+ dot_path = trace_path.with_suffix(".dot")
120
+ dot_path.write_text(dot_content, encoding="utf-8")
121
+
122
+ try:
123
+ subprocess.run(
124
+ ["dot", "-Tpng", str(dot_path), "-o", str(png_path)],
125
+ check=True,
126
+ capture_output=True,
127
+ )
128
+ dot_path.unlink()
129
+ return png_path
130
+ except (subprocess.CalledProcessError, FileNotFoundError):
131
+ dot_path.unlink(missing_ok=True)
132
+ return None
133
+
134
+
135
+ # ---------------------------------------------------------------------------
136
+ # Path reconstruction
137
+ # ---------------------------------------------------------------------------
138
+
139
+ def _extract_execution_path(
140
+ events: list[dict],
141
+ graph: dict,
142
+ ) -> list[tuple[str, str, str]]:
143
+ """
144
+ Reconstruct [(from_node, condition, to_node), ...] from trace events.
145
+
146
+ Uses CC_COMPLETE events (in emission order) and graph edges to walk
147
+ the actual execution path. IN_ boundary nodes always yield ACK in
148
+ the current runtime (admission_snapshot not yet integrated).
149
+
150
+ Args:
151
+ events: Parsed JSONL trace events.
152
+ graph: Compiled graph dict (from graph.json).
153
+
154
+ Returns:
155
+ Ordered list of (from_node_id, condition, to_node_id) tuples.
156
+ """
157
+ entry_node: str = graph["entry"] # e.g. "IN_ACTOR_REGISTERED_V0"
158
+
159
+ # (from_node, condition) → to_node
160
+ edge_map: dict[tuple[str, str], str] = {
161
+ (e["from"], e["condition"]): e["to"]
162
+ for e in graph["edges"]
163
+ }
164
+
165
+ cc_completions = [e for e in events if e["event_type"] == "CC_COMPLETE"]
166
+
167
+ if not cc_completions:
168
+ # No CC nodes executed — IN_ gated as NACK and routed to EXIT
169
+ to_node = edge_map.get((entry_node, "NACK"), "EXIT")
170
+ return [(entry_node, "NACK", to_node)]
171
+
172
+ path: list[tuple[str, str, str]] = []
173
+
174
+ # IN_ → first CC: always ACK (admission passes through in current runtime)
175
+ first_cc_code = cc_completions[0]["detail"]["cc_fqdn"].split("::")[-1]
176
+ path.append((entry_node, "ACK", first_cc_code))
177
+
178
+ # CC → CC (or EXIT) — follow result_status routing
179
+ for cc_event in cc_completions:
180
+ cc_code = cc_event["detail"]["cc_fqdn"].split("::")[-1]
181
+ result_status = cc_event["result_status"]
182
+ to_node = edge_map.get((cc_code, result_status))
183
+ if to_node is None:
184
+ break # no further routing — terminal
185
+ path.append((cc_code, result_status, to_node))
186
+
187
+ return path
188
+
189
+
190
+ # ---------------------------------------------------------------------------
191
+ # DOT generation
192
+ # ---------------------------------------------------------------------------
193
+
194
+ def _generate_dot(
195
+ graph: dict,
196
+ visited_nodes: set[str],
197
+ taken_edges: set[tuple[str, str, str]],
198
+ ) -> str:
199
+ """
200
+ Generate Graphviz DOT with actual execution path highlighted in red.
201
+
202
+ Visited nodes: red border + solid red fill.
203
+ Taken edges: red, bold, red label.
204
+ Unvisited: standard fill, grey border, grey edges.
205
+ """
206
+ lines = [
207
+ f'digraph "{graph["wf_id"]}" {{',
208
+ " rankdir=LR;",
209
+ ' node [fontname="Arial"];',
210
+ "",
211
+ ]
212
+
213
+ # --- Nodes ---
214
+ for node in graph["nodes"]:
215
+ node_id = node["id"]
216
+ node_type = node["type"]
217
+ visited = node_id in visited_nodes
218
+
219
+ if node_type == "IN":
220
+ fill = "tomato" if visited else "lightblue"
221
+ shape = "ellipse"
222
+ elif node_type == "CC":
223
+ fill = "tomato" if visited else "lightgreen"
224
+ shape = "box"
225
+ elif node_type == "EXIT":
226
+ fill = "tomato" if visited else "lightcoral"
227
+ shape = "ellipse"
228
+ else:
229
+ fill = "white"
230
+ shape = "box"
231
+
232
+ if visited:
233
+ lines.append(
234
+ f' "{node_id}" [label="{node_id}", shape={shape},'
235
+ f" style=filled, fillcolor={fill}, color=red, penwidth=2.5];"
236
+ )
237
+ else:
238
+ lines.append(
239
+ f' "{node_id}" [label="{node_id}", shape={shape},'
240
+ f" style=filled, fillcolor={fill}, color=gray];"
241
+ )
242
+
243
+ lines.append("")
244
+
245
+ # --- Edges ---
246
+ for edge in graph["edges"]:
247
+ from_id = edge["from"]
248
+ to_id = edge["to"]
249
+ condition = edge["condition"]
250
+ taken = (from_id, to_id, condition) in taken_edges
251
+
252
+ if taken:
253
+ lines.append(
254
+ f' "{from_id}" -> "{to_id}"'
255
+ f' [label="{condition}", color=red, penwidth=2.5, fontcolor=red];'
256
+ )
257
+ else:
258
+ lines.append(
259
+ f' "{from_id}" -> "{to_id}"'
260
+ f' [label="{condition}", color=gray, fontcolor=gray];'
261
+ )
262
+
263
+ lines.append("}")
264
+ return "\n".join(lines)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgs_runtime
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: PGS runtime for Protocol-Governed Systems
5
5
  Author: Bachi
6
6
  License-Expression: Apache-2.0
@@ -15,6 +15,7 @@ pgs_runtime/loader.py
15
15
  pgs_runtime/memory.py
16
16
  pgs_runtime/scheduler.py
17
17
  pgs_runtime/server.py
18
+ pgs_runtime/trace_viz.py
18
19
  pgs_runtime.egg-info/PKG-INFO
19
20
  pgs_runtime.egg-info/SOURCES.txt
20
21
  pgs_runtime.egg-info/dependency_links.txt
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pgs_runtime"
7
- version = "0.3.1"
7
+ version = "0.4.0"
8
8
  description = "PGS runtime for Protocol-Governed Systems"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
File without changes
File without changes
File without changes
File without changes
File without changes