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
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
trace_examiner — Post-execution diagnostic module.
|
|
3
|
+
|
|
4
|
+
Governed by: STRUCTURE_TRACE_SCHEMA_V0 §10, Trace Examiner spec
|
|
5
|
+
|
|
6
|
+
Public API:
|
|
7
|
+
analyze(trace_path) -> DiagnosticReport
|
|
8
|
+
|
|
9
|
+
Reads completed JSONL trace, classifies failures deterministically,
|
|
10
|
+
resolves artifact paths, generates prescriptive fix hints.
|
|
11
|
+
|
|
12
|
+
Module boundaries (spec §6):
|
|
13
|
+
- No imports from execution/machine/
|
|
14
|
+
- parser.py imports only json, pathlib
|
|
15
|
+
- locator.py imports only structure.path_registry
|
|
16
|
+
- classifier.py, hint_engine.py, reporter.py — pure functions
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from runtime.examine.parser import parse_trace, ParsedTrace, TraceParseError
|
|
24
|
+
from runtime.examine.classifier import classify, FailureClass, STRUCTURAL_FAILURES
|
|
25
|
+
from runtime.examine.locator import locate_artifact
|
|
26
|
+
from runtime.examine.hint_engine import generate_hint
|
|
27
|
+
from runtime.examine.reporter import DiagnosticReport, SideEffectOutcome
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _extract_side_effect_outcomes(trace: ParsedTrace) -> list[SideEffectOutcome]:
|
|
31
|
+
"""
|
|
32
|
+
Extract side-effect outcomes from capability dispatch + node_end correlation.
|
|
33
|
+
|
|
34
|
+
Identifies CC nodes that dispatched capability contracts and correlates
|
|
35
|
+
them with their node_end status to produce business-visible outcomes.
|
|
36
|
+
"""
|
|
37
|
+
dispatched_cc: dict[str, str] = {} # node_id -> cc_code
|
|
38
|
+
for event in trace.capability_dispatch_events:
|
|
39
|
+
p = event.get("payload", {})
|
|
40
|
+
node_id = p.get("node_id", "")
|
|
41
|
+
cc_code = p.get("cc_code", "")
|
|
42
|
+
if node_id and cc_code:
|
|
43
|
+
dispatched_cc[node_id] = cc_code
|
|
44
|
+
|
|
45
|
+
# Correlate with node_end events to get result status
|
|
46
|
+
outcomes: list[SideEffectOutcome] = []
|
|
47
|
+
for event in trace.node_end_events:
|
|
48
|
+
p = event.get("payload", {})
|
|
49
|
+
node_id = p.get("node_id", "")
|
|
50
|
+
if node_id in dispatched_cc:
|
|
51
|
+
outcomes.append(SideEffectOutcome(
|
|
52
|
+
cc_code=dispatched_cc[node_id],
|
|
53
|
+
result_status=p.get("status", "UNKNOWN"),
|
|
54
|
+
))
|
|
55
|
+
|
|
56
|
+
return outcomes
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def analyze(trace_path: Path) -> DiagnosticReport:
|
|
60
|
+
"""
|
|
61
|
+
Analyze a completed trace file and produce a diagnostic report.
|
|
62
|
+
|
|
63
|
+
This is the single public entry point for the Trace Examiner.
|
|
64
|
+
|
|
65
|
+
Snapshot root is derived from the trace path per STRUCTURE layout convention:
|
|
66
|
+
trace_path = {workspace}/traces/{trace_id}/{trace_id}.jsonl
|
|
67
|
+
snapshot = {workspace}/protocol_snapshot
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
trace_path: Path to a completed .jsonl trace file.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
DiagnosticReport with failure classification, artifact path, and fix hint.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
TraceParseError: If the trace file is missing, empty, or malformed.
|
|
77
|
+
"""
|
|
78
|
+
# Derive snapshot root from trace path (STRUCTURE layout: traces/{id}/{id}.jsonl)
|
|
79
|
+
snapshot_root: Path | None = None
|
|
80
|
+
candidate = trace_path.parent.parent.parent / "protocol_snapshot"
|
|
81
|
+
if candidate.is_dir():
|
|
82
|
+
snapshot_root = candidate
|
|
83
|
+
|
|
84
|
+
# 1. Parse
|
|
85
|
+
trace = parse_trace(trace_path)
|
|
86
|
+
|
|
87
|
+
# 2. Classify
|
|
88
|
+
result = classify(trace)
|
|
89
|
+
|
|
90
|
+
# 3. Locate artifact
|
|
91
|
+
artifact_path = None
|
|
92
|
+
if result.failure_class is not None:
|
|
93
|
+
artifact_path = locate_artifact(trace, result, snapshot_root)
|
|
94
|
+
|
|
95
|
+
# 4. Generate hint
|
|
96
|
+
fix_hint = generate_hint(trace, result, artifact_path)
|
|
97
|
+
|
|
98
|
+
# 5. Extract side-effect outcomes
|
|
99
|
+
side_effect_outcomes = _extract_side_effect_outcomes(trace)
|
|
100
|
+
|
|
101
|
+
# 6. Build report
|
|
102
|
+
is_structural = (
|
|
103
|
+
result.failure_class is not None
|
|
104
|
+
and result.failure_class in STRUCTURAL_FAILURES
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return DiagnosticReport(
|
|
108
|
+
execution_id=trace.execution_id,
|
|
109
|
+
workflow_code=trace.workflow_code,
|
|
110
|
+
has_structural_failure=is_structural,
|
|
111
|
+
failure_class=result.failure_class,
|
|
112
|
+
failing_node=result.node_id,
|
|
113
|
+
reason=result.message,
|
|
114
|
+
artifact_path=artifact_path,
|
|
115
|
+
fix_hint=fix_hint,
|
|
116
|
+
side_effect_outcomes=side_effect_outcomes,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
__all__ = [
|
|
121
|
+
"analyze",
|
|
122
|
+
"DiagnosticReport",
|
|
123
|
+
"SideEffectOutcome",
|
|
124
|
+
"FailureClass",
|
|
125
|
+
"TraceParseError",
|
|
126
|
+
]
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""
|
|
2
|
+
classifier.py — Deterministic failure classification.
|
|
3
|
+
|
|
4
|
+
Governed by: STRUCTURE_TRACE_SCHEMA_V0 §10, Trace Examiner spec §4-5
|
|
5
|
+
|
|
6
|
+
Classification is rule-based and deterministic — driven by structured
|
|
7
|
+
trace fields only. Never string-parse error messages.
|
|
8
|
+
|
|
9
|
+
Rules are applied in spec-defined order. First match wins.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from runtime.examine.parser import ParsedTrace, TraceEventDict
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FailureClass(Enum):
|
|
22
|
+
"""Failure classification categories per spec §4."""
|
|
23
|
+
|
|
24
|
+
BUSINESS_VIOLATION = "BUSINESS_VIOLATION"
|
|
25
|
+
EXPRESSION_ERROR = "EXPRESSION_ERROR"
|
|
26
|
+
SCHEMA_ERROR = "SCHEMA_ERROR"
|
|
27
|
+
CT_STRUCTURE_ERROR = "CT_STRUCTURE_ERROR"
|
|
28
|
+
BINDING_ERROR = "BINDING_ERROR"
|
|
29
|
+
CS_RUNTIME_ERROR = "CS_RUNTIME_ERROR"
|
|
30
|
+
GRAPH_STRUCTURE_ERROR = "GRAPH_STRUCTURE_ERROR"
|
|
31
|
+
ADMISSION_ERROR = "ADMISSION_ERROR"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Failure classes that trigger escalation (exit(1)) in authoring mode
|
|
35
|
+
STRUCTURAL_FAILURES = frozenset(FailureClass) - {FailureClass.BUSINESS_VIOLATION}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class ClassificationResult:
|
|
40
|
+
"""Result of classifying a trace."""
|
|
41
|
+
|
|
42
|
+
failure_class: FailureClass | None
|
|
43
|
+
root_event: TraceEventDict | None # The earliest causal event
|
|
44
|
+
is_structural: bool # True if failure_class is not BUSINESS_VIOLATION
|
|
45
|
+
node_id: str | None # Failing node if identifiable
|
|
46
|
+
error_code: str | None # Structured error code if from error event
|
|
47
|
+
message: str # Human-readable reason
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _payload(event: TraceEventDict) -> dict[str, Any]:
|
|
51
|
+
"""Extract payload from event, defaulting to empty dict."""
|
|
52
|
+
return event.get("payload", {})
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _classify_error_event(event: TraceEventDict) -> FailureClass:
|
|
56
|
+
"""
|
|
57
|
+
Classify a single error event per spec §4 rules 1-2, 4-5, 8-9.
|
|
58
|
+
|
|
59
|
+
Rules applied in order:
|
|
60
|
+
Rule 1: error_code == EXPRESSION_RESOLUTION_FAILED → EXPRESSION_ERROR
|
|
61
|
+
Rule 2: error_code == SCHEMA_VALIDATION_FAILED → SCHEMA_ERROR
|
|
62
|
+
Rule 4: node_category == CT → CT_STRUCTURE_ERROR
|
|
63
|
+
Rule 5: node_category == CS → CS_RUNTIME_ERROR
|
|
64
|
+
Rule 8: error_code == BINDING_RESOLUTION_FAILED → BINDING_ERROR
|
|
65
|
+
Rule 9: catch-all error → CT_STRUCTURE_ERROR
|
|
66
|
+
"""
|
|
67
|
+
p = _payload(event)
|
|
68
|
+
error_code = p.get("error_code", "")
|
|
69
|
+
node_category = p.get("node_category", "")
|
|
70
|
+
|
|
71
|
+
# Rule 1
|
|
72
|
+
if error_code == "EXPRESSION_RESOLUTION_FAILED":
|
|
73
|
+
return FailureClass.EXPRESSION_ERROR
|
|
74
|
+
|
|
75
|
+
# Rule 2
|
|
76
|
+
if error_code == "SCHEMA_VALIDATION_FAILED":
|
|
77
|
+
return FailureClass.SCHEMA_ERROR
|
|
78
|
+
|
|
79
|
+
# Rule 4
|
|
80
|
+
if node_category == "CT":
|
|
81
|
+
return FailureClass.CT_STRUCTURE_ERROR
|
|
82
|
+
|
|
83
|
+
# Rule 5
|
|
84
|
+
if node_category == "CS":
|
|
85
|
+
return FailureClass.CS_RUNTIME_ERROR
|
|
86
|
+
|
|
87
|
+
# Rule 8
|
|
88
|
+
if error_code == "BINDING_RESOLUTION_FAILED":
|
|
89
|
+
return FailureClass.BINDING_ERROR
|
|
90
|
+
|
|
91
|
+
# Admission errors (pre-DAG enforcement failures)
|
|
92
|
+
if error_code in ("ADMISSION_BINDING_MISSING_FIELD", "ADMISSION_DENIED"):
|
|
93
|
+
return FailureClass.ADMISSION_ERROR
|
|
94
|
+
|
|
95
|
+
# Rule 9: catch-all structural
|
|
96
|
+
return FailureClass.CT_STRUCTURE_ERROR
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _is_business_violation(event: TraceEventDict, trace: ParsedTrace) -> bool:
|
|
100
|
+
"""
|
|
101
|
+
Check Rule 3: node_end with NACK status on intent node.
|
|
102
|
+
|
|
103
|
+
We need to look up the node_type from the corresponding node_start event.
|
|
104
|
+
"""
|
|
105
|
+
p = _payload(event)
|
|
106
|
+
if event.get("event_type") != "node_end":
|
|
107
|
+
return False
|
|
108
|
+
if p.get("status") != "NACK":
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
# Determine node_type from matching node_start event
|
|
112
|
+
node_id = p.get("node_id", "")
|
|
113
|
+
for ev in trace.events:
|
|
114
|
+
if (
|
|
115
|
+
ev.get("event_type") == "node_start"
|
|
116
|
+
and _payload(ev).get("node_id") == node_id
|
|
117
|
+
):
|
|
118
|
+
if _payload(ev).get("node_type") == "intent":
|
|
119
|
+
return True
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def classify(trace: ParsedTrace) -> ClassificationResult:
|
|
126
|
+
"""
|
|
127
|
+
Classify a parsed trace per spec §4-5.
|
|
128
|
+
|
|
129
|
+
Scans events in trace order. Applies classification rules.
|
|
130
|
+
Returns on first structural match (single-root diagnosis, V0 constraint).
|
|
131
|
+
|
|
132
|
+
If no error event exists, falls back to workflow_complete authority (§5A).
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
trace: Parsed trace from parser.parse_trace()
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
ClassificationResult with failure class, root event, and metadata.
|
|
139
|
+
"""
|
|
140
|
+
# Fast path: successful execution — but scan for unhappy-path exits
|
|
141
|
+
if trace.status == "SUCCESS":
|
|
142
|
+
unhappy = _detect_unhappy_path(trace)
|
|
143
|
+
if unhappy is not None:
|
|
144
|
+
return unhappy
|
|
145
|
+
return ClassificationResult(
|
|
146
|
+
failure_class=None,
|
|
147
|
+
root_event=None,
|
|
148
|
+
is_structural=False,
|
|
149
|
+
node_id=None,
|
|
150
|
+
error_code=None,
|
|
151
|
+
message="Execution completed successfully",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Scan events in sequence order for root cause (spec §5).
|
|
155
|
+
# Parser guarantees events are sorted by sequence number.
|
|
156
|
+
# First structural match wins (single-root, V0).
|
|
157
|
+
for event in trace.events:
|
|
158
|
+
event_type = event.get("event_type")
|
|
159
|
+
p = _payload(event)
|
|
160
|
+
|
|
161
|
+
# Rule 3: business violation check (node_end with NACK on intent)
|
|
162
|
+
if event_type == "node_end" and p.get("status") == "NACK":
|
|
163
|
+
if _is_business_violation(event, trace):
|
|
164
|
+
return ClassificationResult(
|
|
165
|
+
failure_class=FailureClass.BUSINESS_VIOLATION,
|
|
166
|
+
root_event=event,
|
|
167
|
+
is_structural=False,
|
|
168
|
+
node_id=p.get("node_id"),
|
|
169
|
+
error_code=None,
|
|
170
|
+
message=f"Business violation at {p.get('node_id', 'unknown')}: NACK",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Rules 1, 2, 4, 5, 8, 9: error event classification
|
|
174
|
+
if event_type == "error":
|
|
175
|
+
failure_class = _classify_error_event(event)
|
|
176
|
+
return ClassificationResult(
|
|
177
|
+
failure_class=failure_class,
|
|
178
|
+
root_event=event,
|
|
179
|
+
is_structural=failure_class in STRUCTURAL_FAILURES,
|
|
180
|
+
node_id=p.get("node_id"),
|
|
181
|
+
error_code=p.get("error_code"),
|
|
182
|
+
message=p.get("message", "Unknown error"),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Also check node_end with failure status (spec §5 criterion 2)
|
|
186
|
+
# Schema defines lowercase "failed"; also accept uppercase for robustness
|
|
187
|
+
if event_type == "node_end" and p.get("status", "").lower() in ("failed", "failure", "error"):
|
|
188
|
+
# No error event preceded this — classify as generic structural
|
|
189
|
+
return ClassificationResult(
|
|
190
|
+
failure_class=FailureClass.CT_STRUCTURE_ERROR,
|
|
191
|
+
root_event=event,
|
|
192
|
+
is_structural=True,
|
|
193
|
+
node_id=p.get("node_id"),
|
|
194
|
+
error_code=None,
|
|
195
|
+
message=f"Node {p.get('node_id', 'unknown')} ended with {p.get('status')}",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# §5A — Workflow Complete Authority
|
|
199
|
+
# No error event found, but workflow failed. Use exit_reason_code.
|
|
200
|
+
wc = trace.workflow_complete_event
|
|
201
|
+
if wc and trace.status in ("FAILURE", "ABORT", "TIMEOUT"):
|
|
202
|
+
return _classify_from_workflow_complete(trace)
|
|
203
|
+
|
|
204
|
+
# No failure detected despite non-SUCCESS status
|
|
205
|
+
return ClassificationResult(
|
|
206
|
+
failure_class=None,
|
|
207
|
+
root_event=None,
|
|
208
|
+
is_structural=False,
|
|
209
|
+
node_id=None,
|
|
210
|
+
error_code=None,
|
|
211
|
+
message=f"Workflow ended with status {trace.status} but no classifiable failure found",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
_HAPPY_STATUSES = frozenset({"SUCCESS", "ACK", "completed"})
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _detect_unhappy_path(trace: ParsedTrace) -> ClassificationResult | None:
|
|
219
|
+
"""
|
|
220
|
+
Detect unhappy-path exits on SUCCESS workflows.
|
|
221
|
+
|
|
222
|
+
Even when the workflow completes successfully (valid EXIT path), a
|
|
223
|
+
capability contract ending with NOT_FOUND, VIOLATION, etc. may indicate
|
|
224
|
+
an early-exit path. We flag it as BUSINESS_VIOLATION only when the
|
|
225
|
+
non-success CC status led directly to an EXIT/TERMINAL node — meaning
|
|
226
|
+
the workflow terminated early rather than continuing normally.
|
|
227
|
+
"""
|
|
228
|
+
# Build ordered list of node transitions from trace events
|
|
229
|
+
node_sequence: list[tuple[str, str, str]] = [] # (node_id, status, node_type)
|
|
230
|
+
node_types: dict[str, str] = {}
|
|
231
|
+
|
|
232
|
+
for event in trace.events:
|
|
233
|
+
et = event.get("event_type")
|
|
234
|
+
p = _payload(event)
|
|
235
|
+
if et == "node_start":
|
|
236
|
+
node_types[p.get("node_id", "")] = p.get("node_type", "")
|
|
237
|
+
elif et == "node_end":
|
|
238
|
+
nid = p.get("node_id", "")
|
|
239
|
+
node_sequence.append((nid, p.get("status", ""), node_types.get(nid, "")))
|
|
240
|
+
|
|
241
|
+
# Scan for CC nodes with non-happy status that are immediately followed by EXIT
|
|
242
|
+
for i, (node_id, status, node_type) in enumerate(node_sequence):
|
|
243
|
+
if node_type != "capability_contract":
|
|
244
|
+
continue
|
|
245
|
+
if status in _HAPPY_STATUSES:
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
# Check if the very next node is EXIT or TERMINAL
|
|
249
|
+
if i + 1 < len(node_sequence):
|
|
250
|
+
next_node_id = node_sequence[i + 1][0]
|
|
251
|
+
next_node_type = node_types.get(next_node_id, "")
|
|
252
|
+
if next_node_id in ("EXIT", "TERMINAL") or next_node_type == "exit":
|
|
253
|
+
# Find the original node_end event for reporting
|
|
254
|
+
for event in trace.node_end_events:
|
|
255
|
+
if _payload(event).get("node_id") == node_id:
|
|
256
|
+
return ClassificationResult(
|
|
257
|
+
failure_class=FailureClass.BUSINESS_VIOLATION,
|
|
258
|
+
root_event=event,
|
|
259
|
+
is_structural=False,
|
|
260
|
+
node_id=node_id,
|
|
261
|
+
error_code=None,
|
|
262
|
+
message=(
|
|
263
|
+
f"{node_id} returned {status} — workflow took "
|
|
264
|
+
f"early-exit path. Check payload data matches "
|
|
265
|
+
f"expected state"
|
|
266
|
+
),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _classify_from_workflow_complete(trace: ParsedTrace) -> ClassificationResult:
|
|
273
|
+
"""
|
|
274
|
+
Classify failure from workflow_complete event (§5A authority).
|
|
275
|
+
|
|
276
|
+
Used when no error events exist but workflow_complete indicates failure.
|
|
277
|
+
Classification is based on exit_reason_code.
|
|
278
|
+
"""
|
|
279
|
+
wc = trace.workflow_complete_event
|
|
280
|
+
p = _payload(wc) if wc else {}
|
|
281
|
+
exit_reason_code = trace.exit_reason_code
|
|
282
|
+
|
|
283
|
+
# Rules 6, 7: graph structure errors
|
|
284
|
+
if exit_reason_code in ("NO_TRANSITION", "NO_ENTRY_NODE", "NODE_NOT_FOUND", "EXIT_NOT_FOUND"):
|
|
285
|
+
return ClassificationResult(
|
|
286
|
+
failure_class=FailureClass.GRAPH_STRUCTURE_ERROR,
|
|
287
|
+
root_event=wc,
|
|
288
|
+
is_structural=True,
|
|
289
|
+
node_id=None,
|
|
290
|
+
error_code=None,
|
|
291
|
+
message=f"Graph structure error: {exit_reason_code}",
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
if exit_reason_code == "TIMEOUT":
|
|
295
|
+
return ClassificationResult(
|
|
296
|
+
failure_class=None,
|
|
297
|
+
root_event=wc,
|
|
298
|
+
is_structural=False,
|
|
299
|
+
node_id=None,
|
|
300
|
+
error_code=None,
|
|
301
|
+
message="Execution timed out",
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
if exit_reason_code == "ABORT":
|
|
305
|
+
return ClassificationResult(
|
|
306
|
+
failure_class=None,
|
|
307
|
+
root_event=wc,
|
|
308
|
+
is_structural=False,
|
|
309
|
+
node_id=None,
|
|
310
|
+
error_code=None,
|
|
311
|
+
message="Execution aborted by policy",
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Business/policy rejections
|
|
315
|
+
if exit_reason_code in (
|
|
316
|
+
"ADMISSION_DENIED",
|
|
317
|
+
"GOVERNANCE_VIOLATION",
|
|
318
|
+
"EXIT_VIOLATION",
|
|
319
|
+
"EXIT_REJECTED",
|
|
320
|
+
"EXIT_ALREADY_EXISTS",
|
|
321
|
+
):
|
|
322
|
+
return ClassificationResult(
|
|
323
|
+
failure_class=FailureClass.BUSINESS_VIOLATION,
|
|
324
|
+
root_event=wc,
|
|
325
|
+
is_structural=False,
|
|
326
|
+
node_id=None,
|
|
327
|
+
error_code=None,
|
|
328
|
+
message=f"Policy rejection: {exit_reason_code}",
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Backend/infrastructure failure
|
|
332
|
+
if exit_reason_code == "EXIT_BACKEND_ERROR":
|
|
333
|
+
return ClassificationResult(
|
|
334
|
+
failure_class=FailureClass.CS_RUNTIME_ERROR,
|
|
335
|
+
root_event=wc,
|
|
336
|
+
is_structural=True,
|
|
337
|
+
node_id=None,
|
|
338
|
+
error_code=None,
|
|
339
|
+
message=f"Backend error at exit: {trace.exit_condition}",
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# EXECUTION_ERROR or unknown
|
|
343
|
+
return ClassificationResult(
|
|
344
|
+
failure_class=FailureClass.CT_STRUCTURE_ERROR,
|
|
345
|
+
root_event=wc,
|
|
346
|
+
is_structural=True,
|
|
347
|
+
node_id=None,
|
|
348
|
+
error_code=None,
|
|
349
|
+
message=f"Execution failed: {trace.exit_condition}",
|
|
350
|
+
)
|
runtime/examine/cli.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cli.py — Standalone entry point for the trace examiner.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python -m runtime.examine.cli <trace_file.jsonl>
|
|
6
|
+
|
|
7
|
+
Exits 0 on clean execution or business violation.
|
|
8
|
+
Exits 1 on structural failure (bug in protocol wiring, CT, or binding).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from runtime.examine import analyze, TraceParseError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> None:
|
|
18
|
+
if len(sys.argv) != 2:
|
|
19
|
+
print(
|
|
20
|
+
"Usage: python -m runtime.examine.cli <trace_file.jsonl>",
|
|
21
|
+
file=sys.stderr,
|
|
22
|
+
)
|
|
23
|
+
sys.exit(2)
|
|
24
|
+
|
|
25
|
+
trace_path = Path(sys.argv[1])
|
|
26
|
+
|
|
27
|
+
report = analyze(trace_path)
|
|
28
|
+
print(report.format())
|
|
29
|
+
|
|
30
|
+
if report.has_structural_failure:
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
main()
|