pgc-runtime 2.0.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.
- pgc_runtime-2.0.0.dist-info/METADATA +181 -0
- pgc_runtime-2.0.0.dist-info/RECORD +28 -0
- pgc_runtime-2.0.0.dist-info/WHEEL +5 -0
- pgc_runtime-2.0.0.dist-info/entry_points.txt +2 -0
- pgc_runtime-2.0.0.dist-info/licenses/LICENSE +67 -0
- pgc_runtime-2.0.0.dist-info/licenses/NOTICE +11 -0
- pgc_runtime-2.0.0.dist-info/top_level.txt +1 -0
- runtime/__init__.py +0 -0
- runtime/api.py +71 -0
- runtime/boot.py +147 -0
- runtime/cli.py +387 -0
- runtime/conformance.py +249 -0
- runtime/ct_errors.py +33 -0
- runtime/ct_execute.py +84 -0
- runtime/ct_executor.py +312 -0
- runtime/dispatcher.py +448 -0
- runtime/evidence.py +245 -0
- runtime/examine/__init__.py +126 -0
- runtime/examine/classifier.py +350 -0
- runtime/examine/cli.py +35 -0
- runtime/examine/hint_engine.py +245 -0
- runtime/examine/locator.py +148 -0
- runtime/examine/parser.py +223 -0
- runtime/examine/reporter.py +110 -0
- runtime/loader.py +360 -0
- runtime/memory.py +125 -0
- runtime/scheduler.py +264 -0
- runtime/trace_viz.py +276 -0
runtime/ct_execute.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
execute.py — CT-IR execution entry point.
|
|
3
|
+
|
|
4
|
+
Adapter between the dispatcher and CTExecutor.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from runtime.ct_errors import StructuredError
|
|
10
|
+
from runtime.ct_executor import CTExecutor
|
|
11
|
+
|
|
12
|
+
# Module-level executor singleton — avoids re-creating per call,
|
|
13
|
+
# preserves federated IR directory cache and atom loading.
|
|
14
|
+
# Lazy initialization to avoid import-time side effects (bootstrap requirement)
|
|
15
|
+
_executor: CTExecutor | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _get_executor() -> CTExecutor:
|
|
19
|
+
"""Get or create the CT executor singleton (lazy initialization)."""
|
|
20
|
+
global _executor
|
|
21
|
+
if _executor is None:
|
|
22
|
+
_executor = CTExecutor()
|
|
23
|
+
return _executor
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def execute_ct(ct_ir: dict[str, Any], inputs: dict[str, Any]) -> Any:
|
|
27
|
+
"""
|
|
28
|
+
Execute CT-IR and adapt result to CC expectation.
|
|
29
|
+
|
|
30
|
+
CONTRACT (STRICT):
|
|
31
|
+
- CT-IR MUST be pre-validated by compiler before reaching execution
|
|
32
|
+
- CTExecutor blindly executes pre-validated CT-IR
|
|
33
|
+
- CT-IR declares exactly one output
|
|
34
|
+
- That output is returned directly (unwrapped)
|
|
35
|
+
|
|
36
|
+
SOVEREIGNTY: Execution layer is a blind executor.
|
|
37
|
+
Validation is compiler's responsibility.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# ---- Execution (CT-IR assumed pre-validated by compiler) ----
|
|
41
|
+
symbol_table = _get_executor().execute(
|
|
42
|
+
ct_ir=ct_ir,
|
|
43
|
+
inputs=inputs,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# ---- Output adaptation ----
|
|
47
|
+
outputs = ct_ir.get("outputs")
|
|
48
|
+
if not outputs:
|
|
49
|
+
raise StructuredError(
|
|
50
|
+
error_code="CT_EXECUTION_FAILED",
|
|
51
|
+
node_category="CT",
|
|
52
|
+
message="CT must declare at least one output",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Build CT outputs by mapping from symbol table
|
|
56
|
+
ct_outputs = {}
|
|
57
|
+
for output_name, spec in outputs.items():
|
|
58
|
+
from_symbol = spec["from"]
|
|
59
|
+
|
|
60
|
+
if from_symbol not in symbol_table:
|
|
61
|
+
raise StructuredError(
|
|
62
|
+
error_code="CT_EXECUTION_FAILED",
|
|
63
|
+
node_category="CT",
|
|
64
|
+
message=f"CT output symbol '{from_symbol}' was not produced by CT execution",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
symbol_value = symbol_table[from_symbol]
|
|
68
|
+
|
|
69
|
+
# Extract output field from atom result dict
|
|
70
|
+
# Atoms return a single dict containing all output fields
|
|
71
|
+
# e.g., {"valid": True, "failed_rule": None}
|
|
72
|
+
if isinstance(symbol_value, dict):
|
|
73
|
+
if output_name in symbol_value:
|
|
74
|
+
# Multi-output atom: extract this specific output field
|
|
75
|
+
symbol_value = symbol_value[output_name]
|
|
76
|
+
elif len(symbol_value) == 1:
|
|
77
|
+
# Single-output atom: unwrap the single field
|
|
78
|
+
symbol_value = next(iter(symbol_value.values()))
|
|
79
|
+
|
|
80
|
+
ct_outputs[output_name] = symbol_value
|
|
81
|
+
|
|
82
|
+
# Return contract-shaped outputs (always as dict, never unwrap)
|
|
83
|
+
# Contract declares output shape - return exactly that shape
|
|
84
|
+
return ct_outputs
|
runtime/ct_executor.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from runtime.ct_errors import StructuredError
|
|
5
|
+
|
|
6
|
+
# importlib carve-out: permitted here for compile-time-sealed handler_ref execution.
|
|
7
|
+
# This is NOT discovery — the module path is embedded at compile time by materialize.py.
|
|
8
|
+
# Discovery via importlib is forbidden; execution of a sealed handler_ref is not.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CTExecutionError(StructuredError):
|
|
12
|
+
def __init__(self, message: str):
|
|
13
|
+
super().__init__(
|
|
14
|
+
error_code="CT_EXECUTION_FAILED",
|
|
15
|
+
node_category="CT",
|
|
16
|
+
message=message,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CTExecutor:
|
|
21
|
+
def __init__(self):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------
|
|
25
|
+
# Public entrypoint
|
|
26
|
+
# ---------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
def execute(
|
|
29
|
+
self,
|
|
30
|
+
*,
|
|
31
|
+
ct_ir: dict[str, Any],
|
|
32
|
+
inputs: dict[str, Any],
|
|
33
|
+
) -> dict[str, Any]:
|
|
34
|
+
"""
|
|
35
|
+
Execute a CT-IR program.
|
|
36
|
+
|
|
37
|
+
Assumptions:
|
|
38
|
+
- ct_ir is already validated for host invariants
|
|
39
|
+
- atom_stream is structurally valid
|
|
40
|
+
"""
|
|
41
|
+
ctx = _CTContext(inputs=inputs, input_types=ct_ir.get("input_types", {}))
|
|
42
|
+
|
|
43
|
+
steps: list[dict] = ct_ir.get("atom_stream")
|
|
44
|
+
if not steps:
|
|
45
|
+
raise CTExecutionError("CT-IR missing atom_stream")
|
|
46
|
+
|
|
47
|
+
for idx, step in enumerate(steps):
|
|
48
|
+
atom = step.get("atom")
|
|
49
|
+
if not atom:
|
|
50
|
+
raise CTExecutionError(
|
|
51
|
+
f"Missing atom at index {idx}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Check for loop construct
|
|
55
|
+
if "loop" in step:
|
|
56
|
+
self._execute_loop(ctx, step)
|
|
57
|
+
else:
|
|
58
|
+
# ---- IR format execution ----
|
|
59
|
+
# Pass complete step with all metadata + flattened args
|
|
60
|
+
# Step from IR has nested args, adapter expects flattened
|
|
61
|
+
invocation = {
|
|
62
|
+
**step, # Include all metadata (input_types, output_types, etc.)
|
|
63
|
+
**step.get("args", {}), # Flatten args to top level
|
|
64
|
+
"as": step.get("out") # Normalize output key
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
self._execute_handler_ref(ctx, invocation)
|
|
68
|
+
|
|
69
|
+
return ctx._vars
|
|
70
|
+
|
|
71
|
+
def _execute_handler_ref(self, ctx: "_CTContext | _LoopContext", step: dict[str, Any]) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Execute an atom step by dispatching to its compile-time-sealed handler_ref.
|
|
74
|
+
|
|
75
|
+
handler_ref is embedded in CT-IR at compile time by materialize.py.
|
|
76
|
+
No registry lookup. No discovery. Sealed module path only.
|
|
77
|
+
"""
|
|
78
|
+
handler_ref = step.get("handler_ref")
|
|
79
|
+
if not handler_ref:
|
|
80
|
+
raise CTExecutionError(f"CT-IR step missing handler_ref: {step.get('atom')}")
|
|
81
|
+
module_path = handler_ref.get("module")
|
|
82
|
+
callable_name = handler_ref.get("callable")
|
|
83
|
+
if not module_path or not callable_name:
|
|
84
|
+
raise CTExecutionError(f"Incomplete handler_ref on step: {step.get('atom')}")
|
|
85
|
+
|
|
86
|
+
mod = importlib.import_module(module_path)
|
|
87
|
+
execute_fn = getattr(mod, callable_name)
|
|
88
|
+
|
|
89
|
+
# Adapter logic (migrated from atom_registry._register_execute_atom):
|
|
90
|
+
# Resolve $.path references; skip reserved and metadata keys.
|
|
91
|
+
RESERVED_KEYS = {"atom", "molecule", "kind", "as", "out", "loop", "args", "handler_ref", "input_types"}
|
|
92
|
+
resolved_inputs: dict[str, Any] = {}
|
|
93
|
+
for key, value in step.items():
|
|
94
|
+
if key in RESERVED_KEYS:
|
|
95
|
+
continue
|
|
96
|
+
if isinstance(value, str) and value.startswith("$."):
|
|
97
|
+
resolved_inputs[key] = ctx.resolve(value)
|
|
98
|
+
else:
|
|
99
|
+
resolved_inputs[key] = value
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
result = execute_fn(inputs=resolved_inputs)
|
|
103
|
+
except CTExecutionError:
|
|
104
|
+
raise
|
|
105
|
+
except Exception as exc:
|
|
106
|
+
raise CTExecutionError(
|
|
107
|
+
f"Atom raised exception: {step.get('atom')}: {exc}"
|
|
108
|
+
) from exc
|
|
109
|
+
if result is None:
|
|
110
|
+
raise CTExecutionError(f"Atom returned None: {step.get('atom')}")
|
|
111
|
+
out_key = step.get("as") or step.get("out")
|
|
112
|
+
if out_key:
|
|
113
|
+
ctx.set_value(out_key, result)
|
|
114
|
+
|
|
115
|
+
def _execute_loop(
|
|
116
|
+
self,
|
|
117
|
+
ctx: "_CTContext",
|
|
118
|
+
step: dict[str, Any],
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Execute a loop construct."""
|
|
121
|
+
loop_spec = step["loop"]
|
|
122
|
+
out_key = step.get("out")
|
|
123
|
+
|
|
124
|
+
# Resolve the collection to iterate over
|
|
125
|
+
over_path = loop_spec.get("over")
|
|
126
|
+
collection = ctx.resolve(over_path) if over_path else []
|
|
127
|
+
if not isinstance(collection, (list, tuple)):
|
|
128
|
+
raise CTExecutionError(f"Loop 'over' must resolve to a list: {over_path}")
|
|
129
|
+
|
|
130
|
+
iterator_name = loop_spec.get("iterator", "item")
|
|
131
|
+
|
|
132
|
+
# Initialize accumulator
|
|
133
|
+
accumulator_spec = loop_spec.get("accumulator", {})
|
|
134
|
+
accumulator = {}
|
|
135
|
+
for key, value in accumulator_spec.items():
|
|
136
|
+
if isinstance(value, str) and value.startswith("$."):
|
|
137
|
+
# Resolve from results namespace
|
|
138
|
+
if value.startswith("$.results."):
|
|
139
|
+
var_path = value[10:] # Remove $.results.
|
|
140
|
+
parts = var_path.split(".", 1)
|
|
141
|
+
var_name = parts[0]
|
|
142
|
+
remaining = parts[1] if len(parts) > 1 else None
|
|
143
|
+
result = ctx.get_value(var_name)
|
|
144
|
+
if remaining and isinstance(result, dict):
|
|
145
|
+
for p in remaining.split("."):
|
|
146
|
+
result = result.get(p) if isinstance(result, dict) else None
|
|
147
|
+
accumulator[key] = result
|
|
148
|
+
else:
|
|
149
|
+
accumulator[key] = ctx.resolve(value)
|
|
150
|
+
else:
|
|
151
|
+
accumulator[key] = value
|
|
152
|
+
|
|
153
|
+
loop_inputs_spec = loop_spec.get("inputs", {})
|
|
154
|
+
update_spec = loop_spec.get("update_accumulator", {})
|
|
155
|
+
|
|
156
|
+
last_result = None
|
|
157
|
+
|
|
158
|
+
for item in collection:
|
|
159
|
+
# Build inputs for this iteration
|
|
160
|
+
loop_ctx = _LoopContext(ctx, accumulator, iterator_name, item)
|
|
161
|
+
|
|
162
|
+
# Resolve loop inputs
|
|
163
|
+
resolved_inputs = {}
|
|
164
|
+
for key, value in loop_inputs_spec.items():
|
|
165
|
+
if isinstance(value, str) and value.startswith("$."):
|
|
166
|
+
resolved_inputs[key] = loop_ctx.resolve(value)
|
|
167
|
+
else:
|
|
168
|
+
resolved_inputs[key] = value
|
|
169
|
+
|
|
170
|
+
# Build invocation (preserve ALL step metadata: input_types, output_types, etc.)
|
|
171
|
+
# Step from IR has nested args, adapter expects flattened
|
|
172
|
+
invocation = {
|
|
173
|
+
**step, # Include all metadata (input_types, output_types, loop, etc.)
|
|
174
|
+
**resolved_inputs, # Flatten resolved args to top level
|
|
175
|
+
"as": "__loop_result__" # Override output key for loop
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
self._execute_handler_ref(loop_ctx, invocation)
|
|
179
|
+
last_result = loop_ctx.get_value("__loop_result__")
|
|
180
|
+
|
|
181
|
+
# Update accumulator from results
|
|
182
|
+
for acc_key, result_path in update_spec.items():
|
|
183
|
+
if isinstance(result_path, str) and result_path.startswith("$.results."):
|
|
184
|
+
field = result_path[10:] # Remove $.results.
|
|
185
|
+
if isinstance(last_result, dict):
|
|
186
|
+
accumulator[acc_key] = last_result.get(field)
|
|
187
|
+
|
|
188
|
+
# Store final result
|
|
189
|
+
if out_key and last_result is not None:
|
|
190
|
+
ctx.set_value(out_key, last_result)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class _LoopContext:
|
|
194
|
+
"""Context wrapper for loop iterations with accumulator and iterator."""
|
|
195
|
+
|
|
196
|
+
def __init__(self, parent_ctx: "_CTContext", accumulator: dict, iterator_name: str, iterator_value: Any):
|
|
197
|
+
self._parent = parent_ctx
|
|
198
|
+
self._accumulator = accumulator
|
|
199
|
+
self._iterator_name = iterator_name
|
|
200
|
+
self._iterator_value = iterator_value
|
|
201
|
+
self._vars: dict[str, Any] = {}
|
|
202
|
+
|
|
203
|
+
def get_input(self, name: str) -> Any:
|
|
204
|
+
return self._parent.get_input(name)
|
|
205
|
+
|
|
206
|
+
def set_value(self, name: str, value: Any) -> None:
|
|
207
|
+
self._vars[name] = value
|
|
208
|
+
|
|
209
|
+
def get_value(self, name: str) -> Any:
|
|
210
|
+
return self._vars.get(name)
|
|
211
|
+
|
|
212
|
+
def has_value(self, name: str) -> bool:
|
|
213
|
+
return name in self._vars
|
|
214
|
+
|
|
215
|
+
def resolve(self, path: str) -> Any:
|
|
216
|
+
if not path.startswith("$."):
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
parts = path[2:].split(".")
|
|
220
|
+
if not parts:
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
root = parts[0]
|
|
224
|
+
|
|
225
|
+
if root == "accumulator":
|
|
226
|
+
current = self._accumulator
|
|
227
|
+
elif root == "iterator":
|
|
228
|
+
return self._iterator_value
|
|
229
|
+
elif root == "inputs":
|
|
230
|
+
current = self._parent._inputs
|
|
231
|
+
elif root == "results":
|
|
232
|
+
if len(parts) > 1:
|
|
233
|
+
var_name = parts[1]
|
|
234
|
+
if var_name in self._vars:
|
|
235
|
+
current = self._vars[var_name]
|
|
236
|
+
parts = parts[1:]
|
|
237
|
+
elif self._parent.has_value(var_name):
|
|
238
|
+
current = self._parent.get_value(var_name)
|
|
239
|
+
parts = parts[1:]
|
|
240
|
+
else:
|
|
241
|
+
return None
|
|
242
|
+
else:
|
|
243
|
+
return None
|
|
244
|
+
else:
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
for part in parts[1:]:
|
|
248
|
+
if isinstance(current, dict):
|
|
249
|
+
current = current.get(part)
|
|
250
|
+
else:
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
return current
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# ---------------------------------------------------------
|
|
257
|
+
# Internal execution context (CT-local)
|
|
258
|
+
# ---------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
class _CTContext:
|
|
261
|
+
"""_CTContext — isolated CT execution state."""
|
|
262
|
+
|
|
263
|
+
def __init__(self, *, inputs: dict[str, Any], input_types: dict[str, str] | None = None):
|
|
264
|
+
self._inputs = dict(inputs)
|
|
265
|
+
self._vars: dict[str, Any] = {}
|
|
266
|
+
self.input_types = input_types or {}
|
|
267
|
+
|
|
268
|
+
def get_input(self, name: str) -> Any:
|
|
269
|
+
return self._inputs.get(name)
|
|
270
|
+
|
|
271
|
+
def set_value(self, name: str, value: Any) -> None:
|
|
272
|
+
self._vars[name] = value
|
|
273
|
+
|
|
274
|
+
def get_value(self, name: str) -> Any:
|
|
275
|
+
return self._vars.get(name)
|
|
276
|
+
|
|
277
|
+
def has_value(self, name: str) -> bool:
|
|
278
|
+
return name in self._vars
|
|
279
|
+
|
|
280
|
+
def resolve(self, path: str) -> Any:
|
|
281
|
+
"""Resolve a JSONPath-like string (e.g. "$.inputs.foo.bar" or "$.results.var.field")"""
|
|
282
|
+
if not path.startswith("$."):
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
parts = path[2:].split(".")
|
|
286
|
+
if not parts:
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
root = parts[0]
|
|
290
|
+
|
|
291
|
+
if root == "inputs":
|
|
292
|
+
current = self._inputs
|
|
293
|
+
remaining_parts = parts[1:]
|
|
294
|
+
elif root == "results":
|
|
295
|
+
if len(parts) < 2:
|
|
296
|
+
return None
|
|
297
|
+
var_name = parts[1]
|
|
298
|
+
current = self._vars.get(var_name)
|
|
299
|
+
remaining_parts = parts[2:]
|
|
300
|
+
elif root in self._vars:
|
|
301
|
+
current = self._vars[root]
|
|
302
|
+
remaining_parts = parts[1:]
|
|
303
|
+
else:
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
for part in remaining_parts:
|
|
307
|
+
if isinstance(current, dict):
|
|
308
|
+
current = current.get(part)
|
|
309
|
+
else:
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
return current
|