diff --git a/docs/ci/runtime_intelligence_gitlab_artifacts.md b/docs/ci/runtime_intelligence_gitlab_artifacts.md index d4f274a..f72e5b4 100644 --- a/docs/ci/runtime_intelligence_gitlab_artifacts.md +++ b/docs/ci/runtime_intelligence_gitlab_artifacts.md @@ -195,6 +195,14 @@ When that report gate passes, its summary now emits a context for the Lab-owned report, not an AIGuard-owned marker decision. The CI artifact gate is implemented by `scripts/check_runtime_intelligence_ci_artifacts.py`. It runs in the deployment-risk stage and verifies that the collected optional GitLab artifacts include the manifest gate summary, AIGuard handoff alignment artifact, report gate summary, Runtime Intelligence Risk Summary report, portfolio demo status, and the validated contract markers from the bundle manifest gate. This keeps the final CI gate file-based and deterministic without turning GitLab into a runtime control plane. +The final `runtime_intelligence_ci_artifact_gate_summary.md` also preserves the +report gate's `Validated Duration Traceability` section. It repeats +`duration_handoff_alignment`, +`duration_source: source=entrypoint_requested_frames`, +`duration_scope_label: scope_label=source=entrypoint_requested_frames`, and the +`short 96-frame-class replay (96 frames)` label so CI reviewers can confirm +duration handoff/source/scope alignment from the compact deployment-risk +summary before opening the full Lab report. The same CI artifact gate also checks the copied `aiguard_edgeenv_handoff_alignment.json/.md` for Lab report marker context: `lab_expected_report_markers` must match the Lab-owned Runtime Intelligence diff --git a/scripts/check_runtime_intelligence_ci_artifacts.py b/scripts/check_runtime_intelligence_ci_artifacts.py index 810652f..45ca01f 100644 --- a/scripts/check_runtime_intelligence_ci_artifacts.py +++ b/scripts/check_runtime_intelligence_ci_artifacts.py @@ -85,6 +85,13 @@ "edgeenv-smoke-candidate", "edgeenv-smoke-missing", ] +REQUIRED_DURATION_TRACEABILITY_SUMMARY_MARKERS = ( + "## Validated Duration Traceability", + "duration_handoff_alignment: EdgeEnv/AIGuard report context preserved", + "duration_source: source=entrypoint_requested_frames", + "duration_scope_label: scope_label=source=entrypoint_requested_frames", + "duration_label: short 96-frame-class replay (96 frames)", +) def _record(condition: bool, errors: list[str], message: str) -> None: @@ -125,10 +132,18 @@ def _validate_required_files(report_dir: Path, errors: list[str]) -> None: _record((report_dir / name).is_file(), errors, f"missing artifact: {name}") -def _validate_gate_summary(path: Path, errors: list[str], label: str) -> None: +def _validate_runtime_artifact_gate_summary(path: Path, errors: list[str]) -> None: + label = "Runtime Intelligence artifact gate summary" text = _read_text(path, errors, label) - if text: - _record("- Status: passed" in text, errors, f"{label} must have passed status") + if not text: + return + _record("- Status: passed" in text, errors, f"{label} must have passed status") + for marker in REQUIRED_DURATION_TRACEABILITY_SUMMARY_MARKERS: + _record( + marker in text, + errors, + f"{label} missing duration traceability marker: {marker}", + ) def _validate_bundle_manifest_gate_summary(path: Path, errors: list[str]) -> None: @@ -331,6 +346,15 @@ def _write_summary(path: Path, report_dir: Path, errors: list[str]) -> None: lines.append("") lines.extend(f"- {error}" for error in errors) lines.append("") + else: + lines.append("## Validated Duration Traceability") + lines.append("") + lines.extend( + f"- {marker}" + for marker in REQUIRED_DURATION_TRACEABILITY_SUMMARY_MARKERS + if not marker.startswith("## ") + ) + lines.append("") path.parent.mkdir(parents=True, exist_ok=True) path.write_text("\n".join(lines), encoding="utf-8") @@ -345,10 +369,9 @@ def main(report_dir: str, summary_out: str = "") -> int: report_path / "runtime_intelligence_bundle_manifest_gate_summary.md", errors, ) - _validate_gate_summary( + _validate_runtime_artifact_gate_summary( report_path / "runtime_anomaly_gate_summary.md", errors, - "Runtime Intelligence artifact gate summary", ) _validate_aiguard_handoff_alignment( report_path / "aiguard_edgeenv_handoff_alignment.json", diff --git a/tests/test_runtime_intelligence_ci_template.py b/tests/test_runtime_intelligence_ci_template.py index e409b41..9ea51c8 100644 --- a/tests/test_runtime_intelligence_ci_template.py +++ b/tests/test_runtime_intelligence_ci_template.py @@ -6,6 +6,16 @@ REPO_ROOT = Path(__file__).resolve().parents[1] TEMPLATE = REPO_ROOT / "ci" / "gitlab" / "runtime-intelligence-artifacts.yml" DOC = REPO_ROOT / "docs" / "ci" / "runtime_intelligence_gitlab_artifacts.md" +DURATION_TRACEABILITY_SUMMARY = "\n".join( + [ + "- Status: passed", + "## Validated Duration Traceability", + "- duration_handoff_alignment: EdgeEnv/AIGuard report context preserved", + "- duration_source: source=entrypoint_requested_frames", + "- duration_scope_label: scope_label=source=entrypoint_requested_frames", + "- duration_label: short 96-frame-class replay (96 frames)", + ] +) + "\n" def test_runtime_intelligence_gitlab_template_preserves_roadmap_stages(): @@ -71,6 +81,9 @@ def test_runtime_intelligence_gitlab_doc_states_ownership_boundaries(): assert "lab_expected_report_markers" in text assert "lab_report_contract_context" in text assert "aiguard_validates_expected_report_markers" in text + assert "Validated Duration Traceability" in text + assert "duration_handoff_alignment" in text + assert "runtime_intelligence_ci_artifact_gate_summary.md" in text def test_runtime_intelligence_ci_artifact_gate_passes_for_expected_outputs(tmp_path): @@ -173,7 +186,7 @@ def test_runtime_intelligence_ci_artifact_gate_passes_for_expected_outputs(tmp_p encoding="utf-8", ) (report_dir / "runtime_anomaly_gate_summary.md").write_text( - "- Status: passed\n", + DURATION_TRACEABILITY_SUMMARY, encoding="utf-8", ) (report_dir / "aiguard_edgeenv_handoff_alignment.json").write_text( @@ -241,6 +254,35 @@ def test_runtime_intelligence_ci_artifact_gate_passes_for_expected_outputs(tmp_p assert result == 0 summary = summary_path.read_text(encoding="utf-8") assert "- Status: passed" in summary + assert "## Validated Duration Traceability" in summary + assert ( + "duration_handoff_alignment: EdgeEnv/AIGuard report context preserved" + in summary + ) + assert "duration_source: source=entrypoint_requested_frames" in summary + assert ( + "duration_scope_label: scope_label=source=entrypoint_requested_frames" + in summary + ) + assert "duration_label: short 96-frame-class replay (96 frames)" in summary + + (report_dir / "runtime_anomaly_gate_summary.md").write_text( + "- Status: passed\n", + encoding="utf-8", + ) + missing_duration_summary = tmp_path / "ci_artifact_gate_missing_duration.md" + + result = ci_artifact_gate( + report_dir=str(report_dir), + summary_out=str(missing_duration_summary), + ) + + assert result == 2 + missing_summary = missing_duration_summary.read_text(encoding="utf-8") + assert ( + "Runtime Intelligence artifact gate summary missing duration " + "traceability marker: ## Validated Duration Traceability" + ) in missing_summary def test_runtime_intelligence_ci_artifact_gate_fails_for_missing_risk_summary( diff --git a/tests/test_runtime_intelligence_smoke_script.py b/tests/test_runtime_intelligence_smoke_script.py index 23f0f38..affd49a 100644 --- a/tests/test_runtime_intelligence_smoke_script.py +++ b/tests/test_runtime_intelligence_smoke_script.py @@ -148,3 +148,14 @@ def test_runtime_intelligence_smoke_script_runs_artifact_chain(tmp_path): output_dir / "runtime_intelligence_ci_artifact_gate_summary.md" ).read_text(encoding="utf-8") assert "- Status: passed" in ci_summary + assert "## Validated Duration Traceability" in ci_summary + assert ( + "duration_handoff_alignment: EdgeEnv/AIGuard report context preserved" + in ci_summary + ) + assert "duration_source: source=entrypoint_requested_frames" in ci_summary + assert ( + "duration_scope_label: scope_label=source=entrypoint_requested_frames" + in ci_summary + ) + assert "duration_label: short 96-frame-class replay (96 frames)" in ci_summary