diff --git a/README.md b/README.md index c3f8380..41f4526 100644 --- a/README.md +++ b/README.md @@ -502,6 +502,13 @@ If AIGuard preserves EdgeEnv/Orchestrator `candidate_context.producer` lineage, If EdgeEnv preserves an Orchestrator `operation_risk_summary`, Lab shows the compact queue-pressure, max-pressure task, worker-health, and producer/device-local event markers as navigation context in the Runtime Intelligence Risk Summary. These markers help reviewers find the relevant operation evidence, but they do not become EdgeEnv regression deltas, comparability fields, or a deployment decision override. +When EdgeEnv/Orchestrator context includes reviewer-facing duration metadata, +Lab renders a `Runtime replay duration scope` row with `duration_label`, +`duration_class`, and frame count. This helps reviewers distinguish short +96-frame replay, 5-minute-class sustained replay, and quick starter smoke +without changing Lab deployment policy or treating replay duration as a +production readiness claim. + When the EdgeEnv preservation path is present, Lab also renders a `Lab EdgeEnv preservation context` row with `lab_report_preservation_context_present=True`, `lab_preservation=present`, and `lab_context=present`. This keeps Lab's diff --git a/inferedgelab/report/runtime_intelligence.py b/inferedgelab/report/runtime_intelligence.py index f9feebb..5ec6e50 100644 --- a/inferedgelab/report/runtime_intelligence.py +++ b/inferedgelab/report/runtime_intelligence.py @@ -186,6 +186,16 @@ def _append_telemetry_context_rows( ) ) + replay_scope_labels = _runtime_replay_scope_labels(telemetry_context) + if replay_scope_labels: + rows.append( + ( + "Runtime replay duration scope", + "; ".join(replay_scope_labels), + "Duration metadata helps reviewers choose the right replay bundle; it is navigation context and does not change Lab deployment policy.", + ) + ) + coverage_labels = _runtime_telemetry_coverage_labels(telemetry_context) if coverage_labels: rows.append( @@ -397,6 +407,72 @@ def _operation_risk_summary_parts(summary: dict[str, Any]) -> list[str]: return parts +def _runtime_replay_scope_labels(context: dict[str, Any]) -> list[str]: + labels: list[str] = [] + for run_label in ("baseline", "candidate"): + run_context = context.get(run_label) + if not isinstance(run_context, dict): + continue + label = _runtime_replay_scope_label(run_label, run_context) + if label: + labels.append(label) + return labels + + +def _runtime_replay_scope_label(run_label: str, run_context: dict[str, Any]) -> str: + operation_context = run_context.get("orchestrator_operation_context") + if not isinstance(operation_context, dict): + operation_context = {} + candidate_context = operation_context.get("candidate_context") + if not isinstance(candidate_context, dict): + candidate_context = {} + producer = candidate_context.get("producer") + if not isinstance(producer, dict): + producer = {} + operation_summary = operation_context.get("operation_risk_summary") + if not isinstance(operation_summary, dict): + operation_summary = {} + + payloads = [ + run_context, + operation_context, + candidate_context, + producer, + operation_summary, + ] + duration_label = _first_payload_value(payloads, "duration_label") + duration_class = _first_payload_value(payloads, "duration_class") + frames = _first_payload_value(payloads, "frames") + if frames is None: + frames = _first_payload_value(payloads, "requested_frames") + if frames is None: + frames = _first_payload_value(payloads, "frame_count") + + parts: list[str] = [] + if duration_label is not None: + parts.append(f"label={duration_label}") + if duration_class is not None: + parts.append(f"class={duration_class}") + if frames is not None: + parts.append(f"frames={_format_compact_value(frames)}") + if not parts: + return "" + return f"{run_label}: " + ", ".join(parts) + + +def _first_payload_value(payloads: list[dict[str, Any]], field: str) -> Any: + for payload in payloads: + value = payload.get(field) + if value not in (None, ""): + return value + run_summary = payload.get("run_summary") + if isinstance(run_summary, dict): + value = run_summary.get(field) + if value not in (None, ""): + return value + return None + + def _edgeenv_preservation_run_labels(context: dict[str, Any]) -> list[str]: labels: list[str] = [] for run_label in ("baseline", "candidate"): diff --git a/tests/test_report_generators.py b/tests/test_report_generators.py index 2ed1d32..967a5c2 100644 --- a/tests/test_report_generators.py +++ b/tests/test_report_generators.py @@ -340,6 +340,9 @@ def make_edgeenv_regression_with_orchestrator_context() -> dict: context = regression["runtime_telemetry_context"] context["history"]["summary"]["orchestrator_feed_runs"] = 1 context["candidate"]["orchestrator_context_present"] = True + context["candidate"]["frames"] = 96 + context["candidate"]["duration_class"] = "short_96_frame_class" + context["candidate"]["duration_label"] = "short 96-frame-class replay (96 frames)" context["candidate"]["orchestrator_operation_context"] = { "schema_version": "inferedge-orchestrator-edgeenv-runtime-telemetry-feed-v1", "role": "orchestrator_operation_context_for_edgeenv", @@ -829,6 +832,11 @@ def test_generate_compare_markdown_summarizes_orchestrator_context_risk(): "health=worker_health_degraded, device_local_events=15, " "producer_events=7, degraded_workers=vision_agent |" ) in text + assert ( + "| Runtime replay duration scope | candidate: " + "label=short 96-frame-class replay (96 frames), " + "class=short_96_frame_class, frames=96 |" + ) in text assert ( "| Jetson/device-local EdgeEnv preservation run | candidate: " "identity=jetson_device_local_preservation, run=candidate |" @@ -894,6 +902,8 @@ def test_generate_compare_html_summarizes_operation_risk_summary(): ) assert "Runtime Intelligence Risk Summary" in html + assert "Runtime replay duration scope" in html + assert "short 96-frame-class replay (96 frames)" in html assert "Orchestrator operation risk summary" in html assert "Orchestrator task event rollup" in html assert "vision_agent(delay=1,miss=1,max_delay_cycles=3,max_wait_ms=15)" in html