pgc-runtime 6.0.0__tar.gz → 7.0.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 (39) hide show
  1. {pgc_runtime-6.0.0/pgc_runtime.egg-info → pgc_runtime-7.0.0}/PKG-INFO +3 -8
  2. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/README.md +1 -4
  3. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0/pgc_runtime.egg-info}/PKG-INFO +3 -8
  4. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/pgc_runtime.egg-info/SOURCES.txt +0 -1
  5. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/pgc_runtime.egg-info/requires.txt +0 -3
  6. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/pyproject.toml +2 -5
  7. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/api.py +2 -2
  8. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/cli.py +0 -75
  9. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/evidence.py +2 -2
  10. pgc_runtime-6.0.0/runtime/trace_viz.py +0 -233
  11. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/LICENSE +0 -0
  12. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/MANIFEST.in +0 -0
  13. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/NOTICE +0 -0
  14. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/pgc_runtime.egg-info/dependency_links.txt +0 -0
  15. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/pgc_runtime.egg-info/entry_points.txt +0 -0
  16. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/pgc_runtime.egg-info/top_level.txt +0 -0
  17. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/__init__.py +0 -0
  18. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/boot.py +0 -0
  19. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/conformance.py +0 -0
  20. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/coordinator.py +0 -0
  21. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/ct_errors.py +0 -0
  22. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/ct_execute.py +0 -0
  23. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/ct_executor.py +0 -0
  24. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/dispatcher.py +0 -0
  25. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/examine/__init__.py +0 -0
  26. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/examine/cli.py +0 -0
  27. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/examine/parser.py +0 -0
  28. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/examine/reporter.py +0 -0
  29. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/federation/__init__.py +0 -0
  30. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/federation/client.py +0 -0
  31. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/federation/coordinator.py +0 -0
  32. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/federation/defaults.py +0 -0
  33. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/federation/store.py +0 -0
  34. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/federation/worker.py +0 -0
  35. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/loader.py +0 -0
  36. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/memory.py +0 -0
  37. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/replay.py +0 -0
  38. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/runtime/scheduler.py +0 -0
  39. {pgc_runtime-6.0.0 → pgc_runtime-7.0.0}/setup.cfg +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgc-runtime
3
- Version: 6.0.0
3
+ Version: 7.0.0
4
4
  Summary: PGC protocol runtime — executes a sealed snapshot as governed DAG traversal and emits traces (import package: runtime)
5
5
  Author-email: Bhash Ganti <bachipeachy@gmail.com>
6
6
  License-Expression: Apache-2.0
7
7
  Project-URL: Homepage, https://omnibachi.org/
8
8
  Project-URL: Repository, https://github.com/protocol-governed-computing/protocol_runtime
9
- Project-URL: Standard, https://doi.org/10.5281/zenodo.22150616
9
+ Project-URL: Standard, https://doi.org/10.5281/zenodo.22150615
10
10
  Keywords: protocol-governed-computing,pgc,protocol,runtime,workflow,execution
11
11
  Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Intended Audience :: Developers
@@ -22,8 +22,6 @@ License-File: LICENSE
22
22
  License-File: NOTICE
23
23
  Provides-Extra: reference
24
24
  Requires-Dist: pgc-governance>=2.0.0; extra == "reference"
25
- Provides-Extra: render
26
- Requires-Dist: pgc-inspector>=6.0.0; extra == "render"
27
25
  Dynamic: license-file
28
26
 
29
27
  # protocol_runtime
@@ -86,8 +84,6 @@ the data root:
86
84
  ```
87
85
  <data-root>/traces/<domain>/<WF>/<TRACE_ID>/
88
86
  <TRACE_ID>.jsonl append-only execution log, one SCHEMA_TRACE_EVENT_V0 event per line
89
- <TRACE_ID>.png the path the run took, drawn only on request (`run --behavior-logic`,
90
- or `behavior-logic <trace>`)
91
87
 
92
88
  <data-root>/<domain>/<subdomain>/
93
89
  the stores the domain's runtime binding declares
@@ -113,7 +109,6 @@ the data root:
113
109
  | `coordinator` | serve as the coordinating node of a federated node group |
114
110
  | `worker` | serve as a worker node of a federated node group |
115
111
  | `examine` | read a completed trace: its path by node, contracts, results, events and errors |
116
- | `behavior-logic` | render a completed trace as a PNG: the path, each route's outcome, and why each node decided as far as the trace records it (drawn from the inspector's `si.execution.explain`; needs `pgc-runtime[render]`) |
117
112
 
118
113
  `PGC_SNAPSHOT_ROOT` overrides the snapshot location; `PGC_IMPL_ROOTS` is the colon-separated set of
119
114
  roots on `PYTHONPATH` for domain capability implementations.
@@ -216,4 +211,4 @@ Each repository's `VERSION` file holds the workspace's cycle ordinal, which name
216
211
  branch. It is development accounting, and it is never published. The standard the packages
217
212
  implement is a separate artifact on its own track, and none of these numbers is its revision.
218
213
 
219
- The standard these packages implement is published separately: https://doi.org/10.5281/zenodo.22150616
214
+ The standard these packages implement is published separately: https://doi.org/10.5281/zenodo.22150615
@@ -58,8 +58,6 @@ the data root:
58
58
  ```
59
59
  <data-root>/traces/<domain>/<WF>/<TRACE_ID>/
60
60
  <TRACE_ID>.jsonl append-only execution log, one SCHEMA_TRACE_EVENT_V0 event per line
61
- <TRACE_ID>.png the path the run took, drawn only on request (`run --behavior-logic`,
62
- or `behavior-logic <trace>`)
63
61
 
64
62
  <data-root>/<domain>/<subdomain>/
65
63
  the stores the domain's runtime binding declares
@@ -85,7 +83,6 @@ the data root:
85
83
  | `coordinator` | serve as the coordinating node of a federated node group |
86
84
  | `worker` | serve as a worker node of a federated node group |
87
85
  | `examine` | read a completed trace: its path by node, contracts, results, events and errors |
88
- | `behavior-logic` | render a completed trace as a PNG: the path, each route's outcome, and why each node decided as far as the trace records it (drawn from the inspector's `si.execution.explain`; needs `pgc-runtime[render]`) |
89
86
 
90
87
  `PGC_SNAPSHOT_ROOT` overrides the snapshot location; `PGC_IMPL_ROOTS` is the colon-separated set of
91
88
  roots on `PYTHONPATH` for domain capability implementations.
@@ -188,4 +185,4 @@ Each repository's `VERSION` file holds the workspace's cycle ordinal, which name
188
185
  branch. It is development accounting, and it is never published. The standard the packages
189
186
  implement is a separate artifact on its own track, and none of these numbers is its revision.
190
187
 
191
- The standard these packages implement is published separately: https://doi.org/10.5281/zenodo.22150616
188
+ The standard these packages implement is published separately: https://doi.org/10.5281/zenodo.22150615
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgc-runtime
3
- Version: 6.0.0
3
+ Version: 7.0.0
4
4
  Summary: PGC protocol runtime — executes a sealed snapshot as governed DAG traversal and emits traces (import package: runtime)
5
5
  Author-email: Bhash Ganti <bachipeachy@gmail.com>
6
6
  License-Expression: Apache-2.0
7
7
  Project-URL: Homepage, https://omnibachi.org/
8
8
  Project-URL: Repository, https://github.com/protocol-governed-computing/protocol_runtime
9
- Project-URL: Standard, https://doi.org/10.5281/zenodo.22150616
9
+ Project-URL: Standard, https://doi.org/10.5281/zenodo.22150615
10
10
  Keywords: protocol-governed-computing,pgc,protocol,runtime,workflow,execution
11
11
  Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Intended Audience :: Developers
@@ -22,8 +22,6 @@ License-File: LICENSE
22
22
  License-File: NOTICE
23
23
  Provides-Extra: reference
24
24
  Requires-Dist: pgc-governance>=2.0.0; extra == "reference"
25
- Provides-Extra: render
26
- Requires-Dist: pgc-inspector>=6.0.0; extra == "render"
27
25
  Dynamic: license-file
28
26
 
29
27
  # protocol_runtime
@@ -86,8 +84,6 @@ the data root:
86
84
  ```
87
85
  <data-root>/traces/<domain>/<WF>/<TRACE_ID>/
88
86
  <TRACE_ID>.jsonl append-only execution log, one SCHEMA_TRACE_EVENT_V0 event per line
89
- <TRACE_ID>.png the path the run took, drawn only on request (`run --behavior-logic`,
90
- or `behavior-logic <trace>`)
91
87
 
92
88
  <data-root>/<domain>/<subdomain>/
93
89
  the stores the domain's runtime binding declares
@@ -113,7 +109,6 @@ the data root:
113
109
  | `coordinator` | serve as the coordinating node of a federated node group |
114
110
  | `worker` | serve as a worker node of a federated node group |
115
111
  | `examine` | read a completed trace: its path by node, contracts, results, events and errors |
116
- | `behavior-logic` | render a completed trace as a PNG: the path, each route's outcome, and why each node decided as far as the trace records it (drawn from the inspector's `si.execution.explain`; needs `pgc-runtime[render]`) |
117
112
 
118
113
  `PGC_SNAPSHOT_ROOT` overrides the snapshot location; `PGC_IMPL_ROOTS` is the colon-separated set of
119
114
  roots on `PYTHONPATH` for domain capability implementations.
@@ -216,4 +211,4 @@ Each repository's `VERSION` file holds the workspace's cycle ordinal, which name
216
211
  branch. It is development accounting, and it is never published. The standard the packages
217
212
  implement is a separate artifact on its own track, and none of these numbers is its revision.
218
213
 
219
- The standard these packages implement is published separately: https://doi.org/10.5281/zenodo.22150616
214
+ The standard these packages implement is published separately: https://doi.org/10.5281/zenodo.22150615
@@ -24,7 +24,6 @@ runtime/loader.py
24
24
  runtime/memory.py
25
25
  runtime/replay.py
26
26
  runtime/scheduler.py
27
- runtime/trace_viz.py
28
27
  runtime/examine/__init__.py
29
28
  runtime/examine/cli.py
30
29
  runtime/examine/parser.py
@@ -1,6 +1,3 @@
1
1
 
2
2
  [reference]
3
3
  pgc-governance>=2.0.0
4
-
5
- [render]
6
- pgc-inspector>=6.0.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pgc-runtime"
7
- version = "6.0.0"
7
+ version = "7.0.0"
8
8
  description = "PGC protocol runtime — executes a sealed snapshot as governed DAG traversal and emits traces (import package: runtime)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -32,9 +32,6 @@ dependencies = []
32
32
 
33
33
  [project.optional-dependencies]
34
34
  reference = ["pgc-governance>=2.0.0"]
35
- # The PNG projection (`--behavior-logic`, `behavior-logic`) draws the inspector's explanation of a
36
- # run. Execution never imports the inspector; only `runtime.trace_viz` does.
37
- render = ["pgc-inspector>=6.0.0"]
38
35
 
39
36
  [project.scripts]
40
37
  protocol_runtime = "runtime.cli:main"
@@ -42,7 +39,7 @@ protocol_runtime = "runtime.cli:main"
42
39
  [project.urls]
43
40
  Homepage = "https://omnibachi.org/"
44
41
  Repository = "https://github.com/protocol-governed-computing/protocol_runtime"
45
- Standard = "https://doi.org/10.5281/zenodo.22150616"
42
+ Standard = "https://doi.org/10.5281/zenodo.22150615"
46
43
 
47
44
  [tool.setuptools]
48
45
  include-package-data = false
@@ -78,8 +78,8 @@ def run_workflow(
78
78
  writer.error(str(exc))
79
79
  raise
80
80
  finally:
81
- # The JSONL is the evidence. A picture of the path is a view of it, drawn on request
82
- # (`run --behavior-logic`, or `behavior-logic <trace>`), never as a cost of every run.
81
+ # The JSONL is the evidence. A drawing of the path is a view of it, made by the inspector
82
+ # on request, never by the runtime and never as a cost of every run.
83
83
  writer.close()
84
84
 
85
85
  return RunResult(status=status, surface=surface or {}, trace_id=trace_id, trace_dir=trace_dir)
@@ -4,7 +4,6 @@ cli.py — Token-native CLI entry point for the runtime.
4
4
  Commands:
5
5
  run — Execute a workflow against the tokenized snapshot.
6
6
  examine — Analyze a completed trace file and print a diagnostic report.
7
- behavior-logic — Render a completed trace, explained, over its workflow graph (PNG).
8
7
 
9
8
  Execution path (run):
10
9
  1. Load tokenized snapshot for the domain via loader.load_domain()
@@ -13,7 +12,6 @@ Execution path (run):
13
12
  3. Open TraceWriter at traces/<domain>/<wf_code>/<trace_id>/
14
13
  4. Drive workflow topology via scheduler.run_wf()
15
14
  5. Print result summary; exit 1 on non-SUCCESS
16
- 6. If --behavior-logic: render the explained run as a PNG (trace_viz, a client of the inspector)
17
15
 
18
16
  All runtime behavior comes from the compiled tokenized_snapshot.
19
17
  The CLI does not implement any domain logic.
@@ -72,14 +70,6 @@ def _build_parser() -> argparse.ArgumentParser:
72
70
  "Default: sibling ../snapshot",
73
71
  )
74
72
 
75
- # ── run: optional behavior-logic flag ────────────────────────
76
- run_p.add_argument(
77
- "--behavior-logic",
78
- action="store_true",
79
- dest="behavior_logic",
80
- help="Render execution-path PNG after run (requires graphviz)",
81
- )
82
-
83
73
  # ── replay ────────────────────────────────────────────────────
84
74
  rp_p = subs.add_parser(
85
75
  "replay",
@@ -136,23 +126,6 @@ def _build_parser() -> argparse.ArgumentParser:
136
126
  help="Path to a completed .jsonl trace file",
137
127
  )
138
128
 
139
- # ── behavior-logic ────────────────────────────────────────────
140
- bl_p = subs.add_parser(
141
- "behavior-logic",
142
- help="Render execution-path PNG from a completed trace file",
143
- )
144
- bl_p.add_argument(
145
- "trace_file",
146
- metavar="FILE",
147
- help="Path to a completed .jsonl trace file",
148
- )
149
- bl_p.add_argument(
150
- "--snapshot",
151
- metavar="PATH",
152
- help="Absolute path to the assembled snapshot the trace ran against "
153
- "(or set PGC_SNAPSHOT_ROOT; default: the sibling ../snapshot)",
154
- )
155
-
156
129
  return parser
157
130
 
158
131
 
@@ -208,12 +181,6 @@ def _handle_run(args: argparse.Namespace) -> None:
208
181
  print(f"[runtime] Trace dir: {trace_dir}")
209
182
  print()
210
183
 
211
- # Evidence projection: render execution-path PNG if requested
212
- trace_path = trace_dir / f"{trace_id}.jsonl"
213
- png_path = None
214
- if args.behavior_logic:
215
- png_path = _render_behavior_logic(snapshot_root, trace_path, trace_root=data_root)
216
-
217
184
  print("=" * 60)
218
185
  print("[runtime] Workflow Complete")
219
186
  print("=" * 60)
@@ -224,10 +191,6 @@ def _handle_run(args: argparse.Namespace) -> None:
224
191
  if surface:
225
192
  print("Output:")
226
193
  print(_format_surface(surface))
227
- if png_path:
228
- print(f"Graph: {png_path}")
229
- elif args.behavior_logic:
230
- print("Graph: (graphviz not available — PNG skipped)")
231
194
  print("=" * 60)
232
195
 
233
196
  if result_status not in ("SUCCESS", "ALREADY_EXISTS"):
@@ -341,27 +304,6 @@ def _health_line(snapshot_root: Path, booted) -> str:
341
304
  return f"[runtime] ✓ Snapshot healthy — {n} domain(s) resident and hash-verified{gov}. No issues."
342
305
 
343
306
 
344
- def _handle_behavior_logic(args: argparse.Namespace) -> None:
345
- trace_path = Path(args.trace_file)
346
- if not trace_path.exists():
347
- _fatal(f"Trace file not found: {args.trace_file}")
348
-
349
- snapshot_str = args.snapshot or os.environ.get("PGC_SNAPSHOT_ROOT")
350
- snapshot_root = Path(snapshot_str) if snapshot_str else default_snapshot_root()
351
- if not snapshot_root.is_absolute():
352
- _fatal(f"--snapshot must be an absolute path, got: {snapshot_str}")
353
-
354
- png_path = _render_behavior_logic(snapshot_root, trace_path)
355
- if png_path:
356
- print(f"[runtime] Execution path PNG: {png_path}")
357
- else:
358
- print(
359
- "[runtime] Behavior logic render skipped — graphviz (dot) not available.",
360
- file=sys.stderr,
361
- )
362
- sys.exit(1)
363
-
364
-
365
307
  def _handle_examine(args: argparse.Namespace) -> None:
366
308
  trace_path = Path(args.trace_file)
367
309
  if not trace_path.exists():
@@ -391,21 +333,6 @@ def _handle_examine(args: argparse.Namespace) -> None:
391
333
  # Utilities
392
334
  # ---------------------------------------------------------------------------
393
335
 
394
- def _render_behavior_logic(snapshot_root: Path, trace_path: Path,
395
- trace_root: Path | None = None) -> "Path | None":
396
- """Render the explained run as a PNG. Best-effort: a refusal is reported, never drawn around."""
397
- from runtime.trace_viz import render_trace_png
398
- try:
399
- return render_trace_png(snapshot_root, trace_path, trace_root=trace_root)
400
- except ModuleNotFoundError as exc:
401
- print(f"[runtime] Behavior logic render needs the snapshot inspector "
402
- f"(pgc-runtime[render]): {exc}", file=sys.stderr)
403
- return None
404
- except (FileNotFoundError, ValueError) as exc:
405
- print(f"[runtime] Behavior logic render error: {exc}", file=sys.stderr)
406
- return None
407
-
408
-
409
336
  def _load_payload(payload_path: str | None) -> dict:
410
337
  if not payload_path:
411
338
  return {}
@@ -500,8 +427,6 @@ def main() -> None:
500
427
  _handle_worker(args)
501
428
  elif args.command == "examine":
502
429
  _handle_examine(args)
503
- elif args.command == "behavior-logic":
504
- _handle_behavior_logic(args)
505
430
 
506
431
 
507
432
  if __name__ == "__main__":
@@ -19,8 +19,8 @@ The trace file is written to:
19
19
 
20
20
  subdomain is not known to this module — caller passes the full trace dir path.
21
21
 
22
- This module produces the raw JSONL evidence only. Evidence projection
23
- (execution-path PNG overlay) is handled separately in trace_viz.py.
22
+ This module produces the raw JSONL evidence only. A reader draws a run from it through the
23
+ inspector: its workflow page marks the path `si.execution.explain` gives (`/workflow/?trace=`).
24
24
  """
25
25
 
26
26
  from __future__ import annotations
@@ -1,233 +0,0 @@
1
- """
2
- trace_viz.py — Evidence projection: a run drawn over its workflow, with why it went where it went.
3
-
4
- A client of the snapshot inspector, never a second reader of the trace. It asks two questions of
5
- `inspector.api.query` and draws the answers:
6
-
7
- si.behavior_logic.show the workflow's declared graph (every node and edge)
8
- si.execution.explain the run: the path, each route's outcome, each node's determination
9
- as far as the trace records it, captured inputs, how the run ended
10
- ──────────────────────
11
- → <trace_id>.png beside the trace
12
-
13
- It derives nothing. The inspector resolves the workflow's domain, ties the trace to the snapshot
14
- (and refuses a trace produced under another), and separates what the trace recorded from what it
15
- joined from the snapshot. This module only chooses how each answer looks:
16
-
17
- - **recorded** (from the trace) is drawn in red: the path, the outcome on each route taken, a
18
- gate's failed checks, a node's recorded outcome, errors, captured inputs;
19
- - **joined** (from the snapshot) is drawn in grey: each node's declared capability;
20
- - **declared but not taken** is drawn pale, as before;
21
- - a route the run took that the graph does not declare is drawn dashed.
22
-
23
- The inspector is imported here and nowhere else in the runtime: execution never depends on it,
24
- and only this optional projection does (`pgc-runtime[render]`).
25
-
26
- Uses graphviz (dot) — returns None if dot is not available.
27
- """
28
-
29
- from __future__ import annotations
30
-
31
- import subprocess
32
- from html import escape
33
- from pathlib import Path
34
- from typing import Any, Optional
35
-
36
-
37
- class ExplanationRefused(ValueError):
38
- """The inspector would not explain this trace against this snapshot. `str(exc)` is its reason."""
39
-
40
-
41
- # ---------------------------------------------------------------------------
42
- # Public API
43
- # ---------------------------------------------------------------------------
44
-
45
- def render_trace_png(
46
- snapshot_root: Path,
47
- trace_path: Path,
48
- trace_root: Path | None = None,
49
- ) -> Optional[Path]:
50
- """
51
- Render the explained run over its workflow graph, beside the trace.
52
-
53
- Args:
54
- snapshot_root: The assembled snapshot the trace names.
55
- trace_path: The completed .jsonl trace.
56
- trace_root: The root the trace is named under — the run's data root. Defaults to the
57
- trace's own directory, which names the same file.
58
-
59
- Returns:
60
- Path to the PNG, or None if graphviz is unavailable.
61
-
62
- Raises:
63
- FileNotFoundError: trace_path does not exist.
64
- ExplanationRefused: the inspector refused the trace or has no graph for its workflow.
65
- """
66
- from inspector.api import query # optional projection; execution never imports this
67
-
68
- trace_path = Path(trace_path).resolve()
69
- if not trace_path.is_file():
70
- raise FileNotFoundError(f"Trace file not found: {trace_path}")
71
- root = Path(trace_root).resolve() if trace_root is not None else trace_path.parent
72
- reference = trace_path.relative_to(root).as_posix()
73
-
74
- status, explanation = query("si.execution.explain", {"trace": reference}, snapshot_root,
75
- trace_root=root)
76
- if status != "SUCCESS":
77
- raise ExplanationRefused(explanation.get("reason", status))
78
- status, logic = query("si.behavior_logic.show", {"wf": explanation["wf"]}, snapshot_root)
79
- if status != "SUCCESS":
80
- raise ExplanationRefused(logic.get("reason", status))
81
-
82
- dot_content = _generate_dot(logic["graph"], explanation)
83
-
84
- png_path = trace_path.with_suffix(".png")
85
- dot_path = trace_path.with_suffix(".dot")
86
- dot_path.write_text(dot_content, encoding="utf-8")
87
- try:
88
- subprocess.run(
89
- ["dot", "-Tpng", str(dot_path), "-o", str(png_path)],
90
- check=True,
91
- capture_output=True,
92
- )
93
- dot_path.unlink()
94
- return png_path
95
- except (subprocess.CalledProcessError, FileNotFoundError):
96
- dot_path.unlink(missing_ok=True)
97
- return None
98
-
99
-
100
- # ---------------------------------------------------------------------------
101
- # What the explanation says, reduced to what is drawn
102
- # ---------------------------------------------------------------------------
103
-
104
- def _taken(explanation: dict[str, Any]) -> list[tuple[str, str, str]]:
105
- """The path as (from_node, outcome, to_node), in order — read from the explanation's visits.
106
-
107
- A route that reached neither routing nor an ending has no `to`, and the path stops at the node
108
- that produced it. A visit with no node key means the trace predates node keys: refused, because
109
- the path cannot be placed on the graph without guessing.
110
- """
111
- path: list[tuple[str, str, str]] = []
112
- for visit in explanation.get("visits", []):
113
- route = visit.get("route")
114
- if route is None:
115
- continue
116
- if not visit.get("node"):
117
- raise ExplanationRefused("the trace's routes carry no node keys — it predates them; re-run it")
118
- if route.get("to") is None:
119
- break
120
- path.append((visit["node"], route.get("outcome"), route["to"]))
121
- return path
122
-
123
-
124
- def _recorded_lines(visit: dict[str, Any]) -> list[str]:
125
- """What the trace recorded about why this node decided as it did, as label lines."""
126
- lines: list[str] = []
127
- determination = visit.get("determination") or {}
128
- basis = determination.get("basis")
129
- if basis == "recorded admission checks":
130
- failed = determination.get("failed") or []
131
- if failed:
132
- lines += [f"✗ {c['field']}: {c['rule']} {c['expected']}" for c in failed]
133
- else:
134
- lines.append(f"✓ {len(determination.get('checks') or [])} check(s) held")
135
- elif basis == "capability outcome":
136
- lines.append(f"→ {determination.get('outcome')}")
137
- elif basis == "not recorded":
138
- lines.append("checks not recorded")
139
- for atom in visit.get("atoms") or []:
140
- if atom.get("captured"):
141
- name = (atom.get("atom") or "").split("::")[-1]
142
- lines.append(f"captured: {name}{' (replayed)' if atom.get('replayed') else ''}")
143
- for error in visit.get("errors") or []:
144
- lines.append(f"ERROR: {error.get('message')}")
145
- return lines
146
-
147
-
148
- # ---------------------------------------------------------------------------
149
- # DOT generation
150
- # ---------------------------------------------------------------------------
151
-
152
- _SHAPES = {"IN": ("ellipse", "lightblue"), "CC": ("box", "lightgreen"),
153
- "EXIT": ("ellipse", "lightcoral")}
154
-
155
-
156
- def _label(node_id: str, joined: str | None, recorded: list[str]) -> str:
157
- """An HTML-like label: the node, then what was joined (grey), then what was recorded (red)."""
158
- rows = [f"<B>{escape(node_id)}</B>"]
159
- if joined:
160
- rows.append(f'<FONT POINT-SIZE="10" COLOR="gray35"><I>{escape(joined)}</I></FONT>')
161
- rows += [f'<FONT POINT-SIZE="10" COLOR="red3">{escape(line)}</FONT>' for line in recorded]
162
- return "<" + "<BR/>".join(rows) + ">"
163
-
164
-
165
- def _generate_dot(graph: dict[str, Any], explanation: dict[str, Any]) -> str:
166
- """
167
- The declared graph, with the explained run drawn over it.
168
-
169
- Visited nodes: red border and fill, with what the trace recorded about the node.
170
- Taken edges: red, bold, labelled with the outcome that selected them.
171
- Undeclared: a route the run took that the graph does not declare — dashed red.
172
- The deciding node of a declared ending carries a double border.
173
- """
174
- path = _taken(explanation)
175
- visits = {v["node"]: v for v in explanation.get("visits", []) if v.get("node")}
176
- taken_edges = {(f, t, o) for f, o, t in path}
177
- ending = explanation.get("ending") or {}
178
- decided_at = ending.get("decided_at") or ending.get("at")
179
-
180
- tie = (explanation.get("tie") or {}).get("snapshot_id", "")
181
- title = (f"{explanation.get('wf')} · {explanation.get('status')} · "
182
- f"{ending.get('kind', '')}\\nsnapshot {tie[:12]} (claimed by the trace)")
183
- lines = [
184
- f'digraph "{graph["wf_id"]}" {{',
185
- " rankdir=LR;",
186
- f' label="{title}"; labelloc=t; fontname="Arial";',
187
- ' node [fontname="Arial"];',
188
- "",
189
- ]
190
-
191
- declared_ids = set()
192
- for node in graph["nodes"]:
193
- node_id = node["id"]
194
- declared_ids.add(node_id)
195
- shape, fill = _SHAPES.get(node["type"], ("box", "white"))
196
- visit = visits.get(node_id)
197
- capability = node.get("capability")
198
- joined = capability.split("::")[-1] if capability else None
199
- if visit is None:
200
- lines.append(f' "{node_id}" [label={_label(node_id, joined, [])}, shape={shape},'
201
- f" style=filled, fillcolor={fill}, color=gray];")
202
- continue
203
- extra = ", peripheries=2" if node_id == decided_at else ""
204
- style = "filled,dashed" if visit.get("errors") else "filled"
205
- lines.append(f' "{node_id}" [label={_label(node_id, joined, _recorded_lines(visit))},'
206
- f' shape={shape}, style="{style}", fillcolor=mistyrose, color=red,'
207
- f" penwidth=2.5{extra}];")
208
-
209
- # A place the run reached that the graph does not declare is still drawn: it was recorded.
210
- for node_id, visit in visits.items():
211
- if node_id not in declared_ids:
212
- lines.append(f' "{node_id}" [label={_label(node_id, None, _recorded_lines(visit))},'
213
- f' shape=box, style="filled,dashed", fillcolor=mistyrose, color=red];')
214
-
215
- lines.append("")
216
-
217
- declared_edges = set()
218
- for edge in graph["edges"]:
219
- from_id, to_id, condition = edge["from"], edge["to"], edge["condition"]
220
- declared_edges.add((from_id, to_id, condition))
221
- if (from_id, to_id, condition) in taken_edges:
222
- lines.append(f' "{from_id}" -> "{to_id}"'
223
- f' [label="{condition}", color=red, penwidth=2.5, fontcolor=red];')
224
- else:
225
- lines.append(f' "{from_id}" -> "{to_id}"'
226
- f' [label="{condition}", color=gray, fontcolor=gray];')
227
-
228
- for from_id, to_id, condition in sorted(taken_edges - declared_edges):
229
- lines.append(f' "{from_id}" -> "{to_id}"'
230
- f' [label="{condition} (undeclared)", color=red, style=dashed, fontcolor=red];')
231
-
232
- lines.append("}")
233
- return "\n".join(lines)
File without changes
File without changes
File without changes
File without changes
File without changes