pgs-runtime 0.4.0__tar.gz → 0.6.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.
- {pgs_runtime-0.4.0/pgs_runtime.egg-info → pgs_runtime-0.6.0}/PKG-INFO +3 -2
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/README.md +2 -1
- pgs_runtime-0.6.0/pgs_runtime/api.py +61 -0
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime/cli.py +37 -66
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime/conformance.py +19 -2
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime/dispatcher.py +34 -1
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime/loader.py +28 -16
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime/scheduler.py +16 -4
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime/trace_viz.py +21 -9
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0/pgs_runtime.egg-info}/PKG-INFO +3 -2
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime.egg-info/SOURCES.txt +1 -0
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pyproject.toml +1 -1
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/LICENSE +0 -0
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/MANIFEST.in +0 -0
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/NOTICE +0 -0
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime/__init__.py +0 -0
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime/ct_errors.py +0 -0
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime/ct_execute.py +0 -0
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime/ct_executor.py +0 -0
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime/evidence.py +0 -0
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime/examine/__init__.py +0 -0
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime/examine/classifier.py +0 -0
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime/examine/cli.py +0 -0
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime/examine/conformance_test.py +0 -0
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime/examine/hint_engine.py +0 -0
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime/examine/locator.py +0 -0
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime/examine/parser.py +0 -0
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime/examine/reporter.py +0 -0
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime/memory.py +0 -0
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime/server.py +0 -0
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime.egg-info/dependency_links.txt +0 -0
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime.egg-info/entry_points.txt +0 -0
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/pgs_runtime.egg-info/top_level.txt +0 -0
- {pgs_runtime-0.4.0 → pgs_runtime-0.6.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pgs_runtime
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: PGS runtime for Protocol-Governed Systems
|
|
5
5
|
Author: Bachi
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -29,7 +29,7 @@ It does not discover behavior. It does not interpret intent. It does not contain
|
|
|
29
29
|
|
|
30
30
|
Behavior is declared in protocol, executed by runtime, implemented in capabilities, and observed via traces and state.
|
|
31
31
|
|
|
32
|
-
> **New to PGS?** This is one of
|
|
32
|
+
> **New to PGS?** This is one of the repositories in the Protocol-Governed Systems ecosystem.
|
|
33
33
|
> For orientation, architecture overview, and end-to-end execution, start at [pgs_workspace](https://github.com/bachipeachy/pgs_workspace).
|
|
34
34
|
|
|
35
35
|
---
|
|
@@ -172,6 +172,7 @@ PGS is intentionally split into layers:
|
|
|
172
172
|
| **Execution** | **`pgs_runtime` ← here** | **Traverse compiled graph deterministically** |
|
|
173
173
|
| Capabilities | `pgs_capabilities` | Provide CT/CS implementations |
|
|
174
174
|
| Domains | `pgs_blockchain`, `pgs_ai_governance` | Real-world workflows |
|
|
175
|
+
| Change Mgmt | `pgs_change_mgmt` | Governed SDLC — Change Request to Authoring Mandate (new in v0.5.0) |
|
|
175
176
|
| Entry point | `pgs_workspace` | Run and observe |
|
|
176
177
|
|
|
177
178
|
---
|
|
@@ -7,7 +7,7 @@ It does not discover behavior. It does not interpret intent. It does not contain
|
|
|
7
7
|
|
|
8
8
|
Behavior is declared in protocol, executed by runtime, implemented in capabilities, and observed via traces and state.
|
|
9
9
|
|
|
10
|
-
> **New to PGS?** This is one of
|
|
10
|
+
> **New to PGS?** This is one of the repositories in the Protocol-Governed Systems ecosystem.
|
|
11
11
|
> For orientation, architecture overview, and end-to-end execution, start at [pgs_workspace](https://github.com/bachipeachy/pgs_workspace).
|
|
12
12
|
|
|
13
13
|
---
|
|
@@ -150,6 +150,7 @@ PGS is intentionally split into layers:
|
|
|
150
150
|
| **Execution** | **`pgs_runtime` ← here** | **Traverse compiled graph deterministically** |
|
|
151
151
|
| Capabilities | `pgs_capabilities` | Provide CT/CS implementations |
|
|
152
152
|
| Domains | `pgs_blockchain`, `pgs_ai_governance` | Real-world workflows |
|
|
153
|
+
| Change Mgmt | `pgs_change_mgmt` | Governed SDLC — Change Request to Authoring Mandate (new in v0.5.0) |
|
|
153
154
|
| Entry point | `pgs_workspace` | Run and observe |
|
|
154
155
|
|
|
155
156
|
---
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Programmatic runtime entry — run a workflow against a snapshot and return its result surface.
|
|
2
|
+
|
|
3
|
+
This is the in-process core behind `pgs_runtime run`: it loads the domain snapshot, opens a trace, drives
|
|
4
|
+
the workflow topology via `scheduler.run_wf`, and returns the terminal status **and result surface**. The
|
|
5
|
+
CLI is a thin wrapper over it, and out-of-process consumers that need the workflow surface programmatically
|
|
6
|
+
(e.g. the change-management validation pipeline) call this directly instead of shelling out and parsing
|
|
7
|
+
stdout or data files.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from pgs_runtime.evidence import TraceWriter, make_trace_id
|
|
16
|
+
from pgs_runtime.loader import load_domain
|
|
17
|
+
from pgs_runtime.scheduler import run_wf
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class RunResult:
|
|
22
|
+
status: str # terminal workflow outcome (e.g. "SUCCESS", "ACK", "VIOLATION")
|
|
23
|
+
surface: dict[str, Any] # workflow result surface (the observable outputs)
|
|
24
|
+
trace_id: str
|
|
25
|
+
trace_dir: Path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run_workflow(
|
|
29
|
+
*,
|
|
30
|
+
wf_fqdn: str,
|
|
31
|
+
payload: dict[str, Any],
|
|
32
|
+
data_root: str | Path,
|
|
33
|
+
workspace: str | Path,
|
|
34
|
+
) -> RunResult:
|
|
35
|
+
"""Execute a workflow against the snapshot in `workspace` and return its `(status, surface, trace)`.
|
|
36
|
+
|
|
37
|
+
Deterministic and side-effect-scoped: writes only to `data_root` (CS state) and `workspace/traces/`.
|
|
38
|
+
Raises on load/vocab errors and propagates runtime exceptions (after recording them to the trace).
|
|
39
|
+
"""
|
|
40
|
+
workspace = Path(workspace)
|
|
41
|
+
domain = wf_fqdn.split("::")[0]
|
|
42
|
+
pkg = load_domain(workspace, domain)
|
|
43
|
+
|
|
44
|
+
trace_id = make_trace_id(domain, wf_fqdn, payload)
|
|
45
|
+
wf_code = wf_fqdn.split("::")[-1]
|
|
46
|
+
trace_dir = workspace / "traces" / domain / wf_code / trace_id
|
|
47
|
+
trace_dir.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
|
|
49
|
+
wf_addr = pkg.vocab.addr(wf_fqdn) # KeyError if the WF is not in the snapshot vocab
|
|
50
|
+
writer = TraceWriter(trace_dir=trace_dir, trace_id=trace_id, domain=domain,
|
|
51
|
+
wf_addr=wf_addr, wf_fqdn=wf_fqdn)
|
|
52
|
+
try:
|
|
53
|
+
status, surface = run_wf(wf_fqdn=wf_fqdn, payload=payload, pkg=pkg,
|
|
54
|
+
writer=writer, data_root=str(data_root))
|
|
55
|
+
except Exception as exc:
|
|
56
|
+
writer.error(str(exc))
|
|
57
|
+
raise
|
|
58
|
+
finally:
|
|
59
|
+
writer.close()
|
|
60
|
+
|
|
61
|
+
return RunResult(status=status, surface=surface or {}, trace_id=trace_id, trace_dir=trace_dir)
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
cli.py — Token-native CLI entry point for the pgs_runtime.
|
|
3
3
|
|
|
4
4
|
Commands:
|
|
5
|
-
run
|
|
6
|
-
examine
|
|
7
|
-
|
|
5
|
+
run — Execute a workflow against the tokenized snapshot.
|
|
6
|
+
examine — Analyze a completed trace file and print a diagnostic report.
|
|
7
|
+
behavior-logic — Render execution-path PNG from a completed trace file.
|
|
8
8
|
|
|
9
9
|
Execution path (run):
|
|
10
10
|
1. Load tokenized snapshot for the domain via loader.load_domain()
|
|
@@ -13,7 +13,7 @@ Execution path (run):
|
|
|
13
13
|
3. Open TraceWriter at traces/<domain>/<wf_code>/<trace_id>/
|
|
14
14
|
4. Drive workflow topology via scheduler.run_wf()
|
|
15
15
|
5. Print result summary; exit 1 on non-SUCCESS
|
|
16
|
-
6. If --
|
|
16
|
+
6. If --behavior-logic: invoke evidence projection (trace_viz) to render PNG
|
|
17
17
|
|
|
18
18
|
All runtime behavior comes from the compiled tokenized_snapshot.
|
|
19
19
|
The CLI does not implement any domain logic.
|
|
@@ -28,9 +28,7 @@ import sys
|
|
|
28
28
|
import time
|
|
29
29
|
from pathlib import Path
|
|
30
30
|
|
|
31
|
-
from pgs_runtime.
|
|
32
|
-
from pgs_runtime.loader import load_domain
|
|
33
|
-
from pgs_runtime.scheduler import run_wf
|
|
31
|
+
from pgs_runtime.api import run_workflow
|
|
34
32
|
|
|
35
33
|
|
|
36
34
|
# ---------------------------------------------------------------------------
|
|
@@ -71,10 +69,11 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
71
69
|
"tokenized_snapshot/ and traces/ live here",
|
|
72
70
|
)
|
|
73
71
|
|
|
74
|
-
# ── run: optional
|
|
72
|
+
# ── run: optional behavior-logic flag ────────────────────────
|
|
75
73
|
run_p.add_argument(
|
|
76
|
-
"--
|
|
74
|
+
"--behavior-logic",
|
|
77
75
|
action="store_true",
|
|
76
|
+
dest="behavior_logic",
|
|
78
77
|
help="Render execution-path PNG after run (requires graphviz)",
|
|
79
78
|
)
|
|
80
79
|
|
|
@@ -86,17 +85,17 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
86
85
|
help="Path to a completed .jsonl trace file",
|
|
87
86
|
)
|
|
88
87
|
|
|
89
|
-
# ──
|
|
90
|
-
|
|
91
|
-
"
|
|
88
|
+
# ── behavior-logic ────────────────────────────────────────────
|
|
89
|
+
bl_p = subs.add_parser(
|
|
90
|
+
"behavior-logic",
|
|
92
91
|
help="Render execution-path PNG from a completed trace file",
|
|
93
92
|
)
|
|
94
|
-
|
|
93
|
+
bl_p.add_argument(
|
|
95
94
|
"trace_file",
|
|
96
95
|
metavar="FILE",
|
|
97
96
|
help="Path to a completed .jsonl trace file",
|
|
98
97
|
)
|
|
99
|
-
|
|
98
|
+
bl_p.add_argument(
|
|
100
99
|
"--workspace",
|
|
101
100
|
metavar="PATH",
|
|
102
101
|
help="Absolute path to pgs_workspace root (or set PGS_WORKSPACE)",
|
|
@@ -135,62 +134,34 @@ def _handle_run(args: argparse.Namespace) -> None:
|
|
|
135
134
|
# Load payload
|
|
136
135
|
payload = _load_payload(args.payload)
|
|
137
136
|
|
|
138
|
-
#
|
|
137
|
+
# Drive the workflow via the programmatic API (single implementation; also used by the CM validation
|
|
138
|
+
# pipeline). The API loads the snapshot, opens the trace, and returns status + surface.
|
|
139
139
|
print(f"[pgs_runtime] Loading {domain} snapshot...")
|
|
140
|
+
t0 = time.monotonic()
|
|
140
141
|
try:
|
|
141
|
-
|
|
142
|
+
run = run_workflow(wf_fqdn=wf_fqdn, payload=payload,
|
|
143
|
+
data_root=str(data_root), workspace=workspace)
|
|
144
|
+
except KeyError as exc:
|
|
145
|
+
_fatal(f"WF FQDN not in vocab: {exc}")
|
|
142
146
|
except (FileNotFoundError, RuntimeError, ValueError) as exc:
|
|
143
147
|
_fatal(str(exc))
|
|
148
|
+
except Exception as exc:
|
|
149
|
+
_fatal(f"Runtime error: {exc}")
|
|
144
150
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
wf_code = wf_fqdn.split("::")[-1] # e.g. "WF_CREATE_WALLET_V0"
|
|
149
|
-
trace_dir = workspace / "traces" / domain / wf_code / trace_id
|
|
150
|
-
trace_dir.mkdir(parents=True, exist_ok=True)
|
|
151
|
+
result_status, surface = run.status, run.surface
|
|
152
|
+
trace_id, trace_dir = run.trace_id, run.trace_dir
|
|
153
|
+
duration_ms = int((time.monotonic() - t0) * 1000)
|
|
151
154
|
|
|
152
155
|
print(f"[pgs_runtime] Workflow: {wf_fqdn}")
|
|
153
156
|
print(f"[pgs_runtime] Trace ID: {trace_id}")
|
|
154
157
|
print(f"[pgs_runtime] Trace dir: {trace_dir}")
|
|
155
158
|
print()
|
|
156
159
|
|
|
157
|
-
# Resolve WF address for TraceWriter
|
|
158
|
-
try:
|
|
159
|
-
wf_addr = pkg.vocab.addr(wf_fqdn)
|
|
160
|
-
except KeyError:
|
|
161
|
-
_fatal(f"WF FQDN not in vocab: {wf_fqdn!r}")
|
|
162
|
-
|
|
163
|
-
writer = TraceWriter(
|
|
164
|
-
trace_dir = trace_dir,
|
|
165
|
-
trace_id = trace_id,
|
|
166
|
-
domain = domain,
|
|
167
|
-
wf_addr = wf_addr,
|
|
168
|
-
wf_fqdn = wf_fqdn,
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
t0 = time.monotonic()
|
|
172
|
-
try:
|
|
173
|
-
result_status, surface = run_wf(
|
|
174
|
-
wf_fqdn = wf_fqdn,
|
|
175
|
-
payload = payload,
|
|
176
|
-
pkg = pkg,
|
|
177
|
-
writer = writer,
|
|
178
|
-
data_root = str(data_root),
|
|
179
|
-
)
|
|
180
|
-
except Exception as exc:
|
|
181
|
-
writer.error(str(exc))
|
|
182
|
-
writer.close()
|
|
183
|
-
_fatal(f"Runtime error: {exc}")
|
|
184
|
-
finally:
|
|
185
|
-
writer.close()
|
|
186
|
-
|
|
187
|
-
duration_ms = int((time.monotonic() - t0) * 1000)
|
|
188
|
-
|
|
189
160
|
# Evidence projection: render execution-path PNG if requested
|
|
190
161
|
trace_path = trace_dir / f"{trace_id}.jsonl"
|
|
191
162
|
png_path = None
|
|
192
|
-
if args.
|
|
193
|
-
png_path =
|
|
163
|
+
if args.behavior_logic:
|
|
164
|
+
png_path = _render_behavior_logic(workspace, trace_path)
|
|
194
165
|
|
|
195
166
|
print("=" * 60)
|
|
196
167
|
print("[pgs_runtime] Workflow Complete")
|
|
@@ -202,16 +173,16 @@ def _handle_run(args: argparse.Namespace) -> None:
|
|
|
202
173
|
if surface:
|
|
203
174
|
print(f"Output: {json.dumps(surface, separators=(',', ':'))}")
|
|
204
175
|
if png_path:
|
|
205
|
-
print(f"
|
|
206
|
-
elif args.
|
|
207
|
-
print("
|
|
176
|
+
print(f"Graph: {png_path}")
|
|
177
|
+
elif args.behavior_logic:
|
|
178
|
+
print("Graph: (graphviz not available — PNG skipped)")
|
|
208
179
|
print("=" * 60)
|
|
209
180
|
|
|
210
181
|
if result_status not in ("SUCCESS", "ALREADY_EXISTS"):
|
|
211
182
|
sys.exit(1)
|
|
212
183
|
|
|
213
184
|
|
|
214
|
-
def
|
|
185
|
+
def _handle_behavior_logic(args: argparse.Namespace) -> None:
|
|
215
186
|
trace_path = Path(args.trace_file)
|
|
216
187
|
if not trace_path.exists():
|
|
217
188
|
_fatal(f"Trace file not found: {args.trace_file}")
|
|
@@ -223,12 +194,12 @@ def _handle_visualize(args: argparse.Namespace) -> None:
|
|
|
223
194
|
if not workspace.is_absolute():
|
|
224
195
|
_fatal(f"--workspace must be an absolute path, got: {workspace_str}")
|
|
225
196
|
|
|
226
|
-
png_path =
|
|
197
|
+
png_path = _render_behavior_logic(workspace, trace_path)
|
|
227
198
|
if png_path:
|
|
228
199
|
print(f"[pgs_runtime] Execution path PNG: {png_path}")
|
|
229
200
|
else:
|
|
230
201
|
print(
|
|
231
|
-
"[pgs_runtime]
|
|
202
|
+
"[pgs_runtime] Behavior logic render skipped — graphviz (dot) not available.",
|
|
232
203
|
file=sys.stderr,
|
|
233
204
|
)
|
|
234
205
|
sys.exit(1)
|
|
@@ -263,13 +234,13 @@ def _handle_examine(args: argparse.Namespace) -> None:
|
|
|
263
234
|
# Utilities
|
|
264
235
|
# ---------------------------------------------------------------------------
|
|
265
236
|
|
|
266
|
-
def
|
|
237
|
+
def _render_behavior_logic(workspace: Path, trace_path: Path) -> "Path | None":
|
|
267
238
|
"""Invoke evidence projection to render execution-path PNG. Best-effort."""
|
|
268
239
|
from pgs_runtime.trace_viz import render_trace_png
|
|
269
240
|
try:
|
|
270
241
|
return render_trace_png(workspace, trace_path)
|
|
271
242
|
except (FileNotFoundError, ValueError) as exc:
|
|
272
|
-
print(f"[pgs_runtime]
|
|
243
|
+
print(f"[pgs_runtime] Behavior logic render error: {exc}", file=sys.stderr)
|
|
273
244
|
return None
|
|
274
245
|
|
|
275
246
|
|
|
@@ -302,8 +273,8 @@ def main() -> None:
|
|
|
302
273
|
_handle_run(args)
|
|
303
274
|
elif args.command == "examine":
|
|
304
275
|
_handle_examine(args)
|
|
305
|
-
elif args.command == "
|
|
306
|
-
|
|
276
|
+
elif args.command == "behavior-logic":
|
|
277
|
+
_handle_behavior_logic(args)
|
|
307
278
|
|
|
308
279
|
|
|
309
280
|
if __name__ == "__main__":
|
|
@@ -198,10 +198,22 @@ def run(snapshot_root: Path) -> ConformanceResult:
|
|
|
198
198
|
expected = artifact.get("expected", {})
|
|
199
199
|
assertions = artifact.get("assertions", {})
|
|
200
200
|
|
|
201
|
+
expected_outcome = artifact.get("expected_outcome", "SUCCESS")
|
|
202
|
+
|
|
201
203
|
try:
|
|
202
204
|
vars_result = executor.execute(ct_ir=ct_ir, inputs=inputs)
|
|
203
205
|
actual = _resolve_outputs(ct_ir, vars_result)
|
|
204
206
|
|
|
207
|
+
if expected_outcome == "VIOLATION":
|
|
208
|
+
# CT completed without error but a VIOLATION was expected.
|
|
209
|
+
result.failed += 1
|
|
210
|
+
result.cases.append(CaseResult(
|
|
211
|
+
fqdn=fqdn,
|
|
212
|
+
passed=False,
|
|
213
|
+
error="expected CTExecutionError (VIOLATION) but CT completed without raising",
|
|
214
|
+
))
|
|
215
|
+
continue
|
|
216
|
+
|
|
205
217
|
# Structural assertions validate non-deterministic fields by shape/type/size.
|
|
206
218
|
# Fields covered by assertions are excluded from exact-match comparison.
|
|
207
219
|
assertion_error = _assert_structural(actual, assertions) if assertions else None
|
|
@@ -226,7 +238,12 @@ def run(snapshot_root: Path) -> ConformanceResult:
|
|
|
226
238
|
result.cases.append(CaseResult(fqdn=fqdn, passed=True))
|
|
227
239
|
|
|
228
240
|
except CTExecutionError as e:
|
|
229
|
-
|
|
230
|
-
|
|
241
|
+
if expected_outcome == "VIOLATION":
|
|
242
|
+
# CT raised as expected — PASS.
|
|
243
|
+
result.passed += 1
|
|
244
|
+
result.cases.append(CaseResult(fqdn=fqdn, passed=True))
|
|
245
|
+
else:
|
|
246
|
+
result.failed += 1
|
|
247
|
+
result.cases.append(CaseResult(fqdn=fqdn, passed=False, error=str(e)))
|
|
231
248
|
|
|
232
249
|
return result
|
|
@@ -86,6 +86,10 @@ def execute_cc(
|
|
|
86
86
|
surface: dict[str, Any] = {}
|
|
87
87
|
result_status = "SUCCESS"
|
|
88
88
|
|
|
89
|
+
# Build a workflow executor closure for CS types that need nested WF invocation.
|
|
90
|
+
# Injected unconditionally — CSs that don't need it ignore it.
|
|
91
|
+
wf_executor = _make_workflow_executor(pkg, writer, data_root)
|
|
92
|
+
|
|
89
93
|
for step in steps:
|
|
90
94
|
step_addr: int = step["addr"]
|
|
91
95
|
op: str | None = step.get("op")
|
|
@@ -104,7 +108,7 @@ def execute_cc(
|
|
|
104
108
|
else:
|
|
105
109
|
# CS step — controlled side effect via declared handler
|
|
106
110
|
result_status, raw_result = _execute_cs_step(
|
|
107
|
-
step_addr, op, resolved_inputs, rb_addr, pkg, data_root
|
|
111
|
+
step_addr, op, resolved_inputs, rb_addr, pkg, data_root, wf_executor
|
|
108
112
|
)
|
|
109
113
|
|
|
110
114
|
# Apply outputs mapping: {cc_field: "$.capability_result.<ct_field>"} → surface fragment
|
|
@@ -177,6 +181,29 @@ def _execute_ct_step(
|
|
|
177
181
|
return "VIOLATION", {}
|
|
178
182
|
|
|
179
183
|
|
|
184
|
+
def _make_workflow_executor(
|
|
185
|
+
pkg: RuntimePackage,
|
|
186
|
+
writer: TraceWriter,
|
|
187
|
+
data_root: str,
|
|
188
|
+
):
|
|
189
|
+
"""
|
|
190
|
+
Build a workflow executor callable for injection into CS config.
|
|
191
|
+
|
|
192
|
+
The returned callable lets CS implementations invoke sub-workflows without
|
|
193
|
+
importing the scheduler directly (avoids circular imports at module load time).
|
|
194
|
+
|
|
195
|
+
Interface: executor(wf_fqdn: str, payload: dict) -> (result_status: str, surface: dict)
|
|
196
|
+
"""
|
|
197
|
+
def executor(wf_fqdn_or_addr, payload: dict) -> tuple[str, dict]:
|
|
198
|
+
from pgs_runtime.scheduler import run_wf # lazy import — avoids circular dependency
|
|
199
|
+
# Compiler tokenizes nested dict string values to int addresses.
|
|
200
|
+
# CS_WORKFLOW_LOOP_V0 receives int addrs from the compiled mapping; resolve to FQDN here.
|
|
201
|
+
if isinstance(wf_fqdn_or_addr, int):
|
|
202
|
+
wf_fqdn_or_addr = pkg.vocab.fqdn(wf_fqdn_or_addr)
|
|
203
|
+
return run_wf(wf_fqdn_or_addr, payload, pkg, writer, data_root)
|
|
204
|
+
return executor
|
|
205
|
+
|
|
206
|
+
|
|
180
207
|
def _execute_cs_step(
|
|
181
208
|
cs_addr: int,
|
|
182
209
|
op: str,
|
|
@@ -184,6 +211,7 @@ def _execute_cs_step(
|
|
|
184
211
|
rb_addr: int,
|
|
185
212
|
pkg: RuntimePackage,
|
|
186
213
|
data_root: str,
|
|
214
|
+
wf_executor=None,
|
|
187
215
|
) -> tuple[str, dict[str, Any]]:
|
|
188
216
|
"""
|
|
189
217
|
Execute a CS (side effect) step.
|
|
@@ -205,6 +233,11 @@ def _execute_cs_step(
|
|
|
205
233
|
policy_raw = policy_entry.get("policy") or {}
|
|
206
234
|
policy = _expand_policy(policy_raw, data_root)
|
|
207
235
|
|
|
236
|
+
# Inject workflow executor for CS types that need nested WF invocation.
|
|
237
|
+
# Injected unconditionally — CSs that don't use it ignore the key.
|
|
238
|
+
if wf_executor is not None:
|
|
239
|
+
policy = {**policy, "workflow_executor": wf_executor}
|
|
240
|
+
|
|
208
241
|
# Instantiate CS runtime
|
|
209
242
|
handler_ref = cs_entry["handler_ref"]
|
|
210
243
|
cs_metadata = cs_entry.get("cs_metadata", {})
|
|
@@ -44,10 +44,10 @@ class DispatchTable:
|
|
|
44
44
|
"""
|
|
45
45
|
Integer-keyed routing substrate from dispatch.json.
|
|
46
46
|
|
|
47
|
-
routing: {wf_addr: {cc_addr: {condition_addr: next_cc_addr}}}
|
|
47
|
+
routing: {wf_addr: {cc_addr: {condition_addr: {"addr": next_cc_addr, "key": node_key}}}}
|
|
48
48
|
pipeline: {cc_addr: [step, ...]}
|
|
49
|
-
entry: {wf_addr: {"start": cc_addr, "rb": rb_addr, "in": in_addr}}
|
|
50
|
-
bindings: {wf_addr: {
|
|
49
|
+
entry: {wf_addr: {"start": cc_addr, "start_key": node_key, "rb": rb_addr, "in": in_addr}}
|
|
50
|
+
bindings: {wf_addr: {node_key: {input_name: path_or_literal}}}
|
|
51
51
|
|
|
52
52
|
Each pipeline step is a named-field execution instruction record:
|
|
53
53
|
{
|
|
@@ -59,14 +59,19 @@ class DispatchTable:
|
|
|
59
59
|
"step_id": str, # symbolic step name for $.results.<step_id>.<field> references
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
Routing values carry both the next CC address and the target node_key so that
|
|
63
|
+
the scheduler can disambiguate distinct WF usages of the same shared CC
|
|
64
|
+
(e.g. four denial audit nodes that all bind to CC_RECORD_DENIED_ACTION_V0).
|
|
65
|
+
|
|
66
|
+
Bindings are keyed by node_key (not CC address) for the same reason.
|
|
67
|
+
|
|
62
68
|
All semantics are compiler-materialized. The dispatcher is a blind executor.
|
|
63
|
-
All dict keys are ints. All address values are ints.
|
|
64
69
|
The nested dicts are plain Python dicts (not frozen) — callers must not mutate.
|
|
65
70
|
"""
|
|
66
|
-
routing: dict[int, dict[int, dict[int,
|
|
71
|
+
routing: dict[int, dict[int, dict[int, Any]]]
|
|
67
72
|
pipeline: dict[int, list[dict]]
|
|
68
|
-
entry: dict[int, dict[str,
|
|
69
|
-
bindings: dict[int, dict[
|
|
73
|
+
entry: dict[int, dict[str, Any]]
|
|
74
|
+
bindings: dict[int, dict[str, dict[str, Any]]]
|
|
70
75
|
|
|
71
76
|
|
|
72
77
|
@dataclass(frozen=True)
|
|
@@ -209,14 +214,16 @@ def _build_dispatch(raw: dict) -> DispatchTable:
|
|
|
209
214
|
"""
|
|
210
215
|
Parse dispatch.json into integer-keyed DispatchTable.
|
|
211
216
|
|
|
212
|
-
JSON keys are strings (JSON spec). Addresses are
|
|
213
|
-
|
|
217
|
+
JSON keys are strings (JSON spec). Addresses are int values.
|
|
218
|
+
Outer WF and CC keys are converted to int. Routing values are
|
|
219
|
+
{"addr": int, "key": str} dicts — preserved as-is. Bindings keys
|
|
220
|
+
are node_key strings — preserved as-is (not int-converted).
|
|
214
221
|
"""
|
|
215
|
-
routing: dict[int, dict[int, dict[int,
|
|
222
|
+
routing: dict[int, dict[int, dict[int, Any]]] = {}
|
|
216
223
|
for wf_key, cc_map in raw.get("routing", {}).items():
|
|
217
224
|
wf_addr = int(wf_key)
|
|
218
225
|
routing[wf_addr] = {
|
|
219
|
-
int(cc_key): {int(cond):
|
|
226
|
+
int(cc_key): {int(cond): tgt for cond, tgt in cond_map.items()}
|
|
220
227
|
for cc_key, cond_map in cc_map.items()
|
|
221
228
|
}
|
|
222
229
|
|
|
@@ -225,15 +232,20 @@ def _build_dispatch(raw: dict) -> DispatchTable:
|
|
|
225
232
|
cc_addr = int(cc_key)
|
|
226
233
|
pipeline[cc_addr] = steps # list of named-field step dicts
|
|
227
234
|
|
|
228
|
-
entry: dict[int, dict[str,
|
|
235
|
+
entry: dict[int, dict[str, Any]] = {}
|
|
229
236
|
for wf_key, e in raw.get("entry", {}).items():
|
|
230
237
|
wf_addr = int(wf_key)
|
|
231
|
-
|
|
238
|
+
# int-convert only numeric values (start, rb, in); leave start_key as str
|
|
239
|
+
entry[wf_addr] = {
|
|
240
|
+
k: (int(v) if isinstance(v, (int, float)) else v)
|
|
241
|
+
for k, v in e.items()
|
|
242
|
+
}
|
|
232
243
|
|
|
233
|
-
bindings: dict[int, dict[
|
|
234
|
-
for wf_key,
|
|
244
|
+
bindings: dict[int, dict[str, dict[str, Any]]] = {}
|
|
245
|
+
for wf_key, node_map in raw.get("bindings", {}).items():
|
|
235
246
|
wf_addr = int(wf_key)
|
|
236
|
-
|
|
247
|
+
# node_map keys are node_key strings (e.g. "CC_NORMALIZE_AGENT_REQUEST_V0")
|
|
248
|
+
bindings[wf_addr] = {node_key: inp for node_key, inp in node_map.items()}
|
|
237
249
|
|
|
238
250
|
return DispatchTable(
|
|
239
251
|
routing = routing,
|
|
@@ -90,6 +90,7 @@ def run_wf(
|
|
|
90
90
|
|
|
91
91
|
rb_addr = entry["rb"]
|
|
92
92
|
current_addr: int | None = entry["start"]
|
|
93
|
+
current_node_key: str = entry.get("start_key", "")
|
|
93
94
|
|
|
94
95
|
ctx = ExecutionContext(payload)
|
|
95
96
|
writer.wf_start(payload)
|
|
@@ -107,11 +108,14 @@ def run_wf(
|
|
|
107
108
|
hops += 1
|
|
108
109
|
|
|
109
110
|
if current_addr in pkg.dispatch.pipeline:
|
|
110
|
-
# CC node — resolve WF-level bindings and execute
|
|
111
|
+
# CC node — resolve WF-level bindings and execute.
|
|
112
|
+
# Bindings are keyed by node_key (not CC addr) so that distinct WF
|
|
113
|
+
# usages of the same CC (e.g. four denial audit nodes) each carry
|
|
114
|
+
# their own literal inputs (e.g. different denial_reason values).
|
|
111
115
|
wf_bindings = (
|
|
112
116
|
pkg.dispatch.bindings
|
|
113
117
|
.get(wf_addr, {})
|
|
114
|
-
.get(
|
|
118
|
+
.get(current_node_key, {})
|
|
115
119
|
)
|
|
116
120
|
cc_inputs = ctx.resolve_inputs(wf_bindings)
|
|
117
121
|
|
|
@@ -125,10 +129,18 @@ def run_wf(
|
|
|
125
129
|
# admission_snapshot not yet integrated; IN_ nodes pass as ACK
|
|
126
130
|
result_status = "ACK"
|
|
127
131
|
|
|
128
|
-
# Resolve result_status → condition address and route to next node
|
|
132
|
+
# Resolve result_status → condition address and route to next node.
|
|
133
|
+
# Routing values are {"addr": int, "key": str} — addr is the next CC address,
|
|
134
|
+
# key is the next node_key for bindings disambiguation.
|
|
129
135
|
condition_addr = _condition_addr(result_status, pkg)
|
|
130
136
|
routing = pkg.dispatch.routing.get(wf_addr, {}).get(current_addr, {})
|
|
131
|
-
|
|
137
|
+
next_entry = routing.get(condition_addr) # None → terminal
|
|
138
|
+
if isinstance(next_entry, dict):
|
|
139
|
+
current_addr = next_entry.get("addr")
|
|
140
|
+
current_node_key = next_entry.get("key", "")
|
|
141
|
+
else:
|
|
142
|
+
current_addr = next_entry # bare int (legacy) or None
|
|
143
|
+
current_node_key = ""
|
|
132
144
|
|
|
133
145
|
writer.wf_complete(result_status)
|
|
134
146
|
return result_status, surface
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""
|
|
2
|
-
trace_viz.py — Evidence projection: execution path
|
|
2
|
+
trace_viz.py — Evidence projection: execution path behavior logic.
|
|
3
3
|
|
|
4
4
|
Reads a completed trace (.jsonl) and the compiled workflow graph (.graph.json)
|
|
5
|
-
from the protocol snapshot
|
|
5
|
+
from the protocol snapshot behavior_logic directory, then generates a PNG with
|
|
6
6
|
the actual execution path overlaid in red on the static compiled graph.
|
|
7
7
|
|
|
8
8
|
This is Evidence Projection — not a runtime execution feature:
|
|
@@ -14,7 +14,7 @@ This is Evidence Projection — not a runtime execution feature:
|
|
|
14
14
|
→ execution path PNG
|
|
15
15
|
|
|
16
16
|
Inputs (both already materialized, read-only):
|
|
17
|
-
protocol_snapshot/
|
|
17
|
+
protocol_snapshot/behavior_logic/<WF_CODE>/<WF_CODE>.graph.json
|
|
18
18
|
traces/<domain>/<wf_code>/<trace_id>/<trace_id>.jsonl
|
|
19
19
|
|
|
20
20
|
Output:
|
|
@@ -22,10 +22,10 @@ Output:
|
|
|
22
22
|
|
|
23
23
|
Architectural invariant:
|
|
24
24
|
This module reads ONLY from:
|
|
25
|
-
- protocol_snapshot/
|
|
25
|
+
- protocol_snapshot/behavior_logic/ (compiled graph artifacts)
|
|
26
26
|
- the caller-supplied trace .jsonl (execution evidence)
|
|
27
27
|
It does NOT walk protocol_snapshot/artifacts/ or any other canonical
|
|
28
|
-
protocol location.
|
|
28
|
+
protocol location. Behavior logic overlay is read-only — no protocol interpretation.
|
|
29
29
|
|
|
30
30
|
Uses graphviz (dot) — returns None silently if dot is not available.
|
|
31
31
|
"""
|
|
@@ -84,18 +84,18 @@ def render_trace_png(
|
|
|
84
84
|
wf_fqdn = wf_start["detail"]["wf_fqdn"]
|
|
85
85
|
wf_code = wf_fqdn.split("::")[-1] # e.g. "WF_REGISTER_ACTOR_UNVERIFIED_V0"
|
|
86
86
|
|
|
87
|
-
# Load compiled graph from protocol_snapshot/
|
|
87
|
+
# Load compiled graph from protocol_snapshot/behavior_logic/
|
|
88
88
|
graph_path = (
|
|
89
89
|
workspace
|
|
90
90
|
/ "protocol_snapshot"
|
|
91
|
-
/ "
|
|
91
|
+
/ "behavior_logic"
|
|
92
92
|
/ wf_code
|
|
93
93
|
/ f"{wf_code}.graph.json"
|
|
94
94
|
)
|
|
95
95
|
if not graph_path.exists():
|
|
96
96
|
raise FileNotFoundError(
|
|
97
97
|
f"Compiled graph not found: {graph_path}\n"
|
|
98
|
-
f"Re-run the compiler to regenerate
|
|
98
|
+
f"Re-run the compiler to regenerate behavior_logic artifacts."
|
|
99
99
|
)
|
|
100
100
|
|
|
101
101
|
graph = json.loads(graph_path.read_text(encoding="utf-8"))
|
|
@@ -162,7 +162,19 @@ def _extract_execution_path(
|
|
|
162
162
|
for e in graph["edges"]
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
# Filter to top-level workflow CC events only.
|
|
166
|
+
# Sub-workflows emit CC_COMPLETE events with the same wf_addr as the
|
|
167
|
+
# top-level workflow (the runtime reuses the same address), so wf_addr
|
|
168
|
+
# filtering is insufficient. Instead, restrict to CCs that are declared
|
|
169
|
+
# nodes in the top-level graph — sub-workflow CCs won't appear there.
|
|
170
|
+
graph_cc_nodes: set[str] = {
|
|
171
|
+
n["id"] for n in graph["nodes"] if n["type"] == "CC"
|
|
172
|
+
}
|
|
173
|
+
cc_completions = [
|
|
174
|
+
e for e in events
|
|
175
|
+
if e["event_type"] == "CC_COMPLETE"
|
|
176
|
+
and e["detail"]["cc_fqdn"].split("::")[-1] in graph_cc_nodes
|
|
177
|
+
]
|
|
166
178
|
|
|
167
179
|
if not cc_completions:
|
|
168
180
|
# No CC nodes executed — IN_ gated as NACK and routed to EXIT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pgs_runtime
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: PGS runtime for Protocol-Governed Systems
|
|
5
5
|
Author: Bachi
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -29,7 +29,7 @@ It does not discover behavior. It does not interpret intent. It does not contain
|
|
|
29
29
|
|
|
30
30
|
Behavior is declared in protocol, executed by runtime, implemented in capabilities, and observed via traces and state.
|
|
31
31
|
|
|
32
|
-
> **New to PGS?** This is one of
|
|
32
|
+
> **New to PGS?** This is one of the repositories in the Protocol-Governed Systems ecosystem.
|
|
33
33
|
> For orientation, architecture overview, and end-to-end execution, start at [pgs_workspace](https://github.com/bachipeachy/pgs_workspace).
|
|
34
34
|
|
|
35
35
|
---
|
|
@@ -172,6 +172,7 @@ PGS is intentionally split into layers:
|
|
|
172
172
|
| **Execution** | **`pgs_runtime` ← here** | **Traverse compiled graph deterministically** |
|
|
173
173
|
| Capabilities | `pgs_capabilities` | Provide CT/CS implementations |
|
|
174
174
|
| Domains | `pgs_blockchain`, `pgs_ai_governance` | Real-world workflows |
|
|
175
|
+
| Change Mgmt | `pgs_change_mgmt` | Governed SDLC — Change Request to Authoring Mandate (new in v0.5.0) |
|
|
175
176
|
| Entry point | `pgs_workspace` | Run and observe |
|
|
176
177
|
|
|
177
178
|
---
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|