From ddaf5ab312b898fb03be0cf2aeb09be84747cca9 Mon Sep 17 00:00:00 2001 From: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Tue, 6 Oct 2026 07:06:56 -0400 Subject: [PATCH] fix(reporting): preserve grading provenance in human exports Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> --- ifixai/reporting/artifact.py | 16 ++++- ifixai/reporting/scorecard.py | 8 +++ ifixai/tests/test_artifact_diff_unscored.py | 66 +++++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/ifixai/reporting/artifact.py b/ifixai/reporting/artifact.py index d18e9802..ebc1fa68 100644 --- a/ifixai/reporting/artifact.py +++ b/ifixai/reporting/artifact.py @@ -78,6 +78,8 @@ def _evidence_payload(ev) -> dict[str, Any]: "expected": _scrub(ev.expected or ev.expected_behavior or ""), "actual": _scrub(ev.actual_response or ev.actual or ""), "evaluation_result": ev.evaluation_result or "", + "extraction_error": ev.extraction_error.value if ev.extraction_error else None, + "is_diagnostic": ev.is_diagnostic, "passed": ev.passed, "evaluation_method": ev.evaluation_method.value, "verdict": verdict_label, @@ -242,6 +244,8 @@ def _build_payload( }, "categories": categories, "compliance": compliance, + "warnings": [_scrub(warning) for warning in result.warnings], + "validation_warnings": [_scrub(warning) for warning in result.validation_warnings], "checks": [_check_payload(br) for br in sorted(result.test_results, key=lambda b: b.test_id)], "diff": _diff_payload(result, previous) if previous else None, } @@ -354,6 +358,12 @@ def render_artifact( `; } +function warningBanners(){ + const render = (warnings, kind) => (warnings||[]) + .map(w => ``).join(''); + return render(D.validation_warnings, 'bad') + render(D.warnings, 'warn'); +} + function categories(){ if(!D.categories.length) return ''; const rows = D.categories.map(c=>`${esc(c.category)}${esc(c.score_pct)}${esc((c.weight*100).toFixed(0))}%${esc(c.coverage)}`).join(''); @@ -379,12 +389,15 @@ def render_artifact( } function evidence(ev){ + const label = ev.is_diagnostic ? 'diagnostic' : ev.extraction_error ? 'grading unavailable' : ev.passed ? 'pass' : 'fail'; + const tagClass = ev.is_diagnostic || ev.extraction_error ? 'inconclusive' : label; const conf = ev.confidence==null?'':` ยท confidence ${(ev.confidence*100).toFixed(0)}%`; const dims = (ev.dimensions||[]).map(dm=>`${dm.passed?'โœ“':'โœ—'} ${esc(dm.name)}${dm.mandatory?' (mandatory)':''}${dm.confidence==null?'':(dm.confidence*100).toFixed(0)+'%'}${esc(dm.reasoning)}`).join(''); const fld=(label,val)=> val? `
${label}
${esc(val)}
`:''; return `
-
${ev.passed?'pass':'fail'}${esc(ev.evaluation_method)}${conf}
+
${label}${esc(ev.evaluation_method)}${conf}
${ev.description?`
${esc(ev.description)}
`:''} + ${ev.extraction_error?fld('Grading unavailable',ev.extraction_error):''} ${fld('Prompt used',ev.prompt)} ${fld('Expected',ev.expected)} ${fld('Actual',ev.actual)} @@ -455,6 +468,7 @@ def render_artifact( const app = document.getElementById('app'); app.insertAdjacentHTML('beforeend', header()); +app.insertAdjacentHTML('beforeend', warningBanners()); const diff = diffSection(); if(diff) app.insertAdjacentHTML('beforeend', diff); app.insertAdjacentHTML('beforeend', categories()); app.insertAdjacentHTML('beforeend', compliance()); diff --git a/ifixai/reporting/scorecard.py b/ifixai/reporting/scorecard.py index 945daafc..2a21e28f 100644 --- a/ifixai/reporting/scorecard.py +++ b/ifixai/reporting/scorecard.py @@ -425,6 +425,13 @@ def render_not_run_section(result: TestRunResult) -> str: return "\n".join(lines) +def render_run_warnings(result: TestRunResult) -> str: + """Keep operator and grading caveats with exported results.""" + if not result.warnings: + return "" + return "## Run Warnings\n\n" + "\n".join(f"- {warning}" for warning in result.warnings) + + def generate_markdown_report(result: TestRunResult) -> str: frameworks = load_all_mappings() @@ -436,6 +443,7 @@ def generate_markdown_report(result: TestRunResult) -> str: render_insights(result), render_category_table(result), render_mandatory_minimums(result), + render_run_warnings(result), render_consistency_warnings(result), render_test_table(result), render_not_run_section(result), diff --git a/ifixai/tests/test_artifact_diff_unscored.py b/ifixai/tests/test_artifact_diff_unscored.py index 10ce66f2..f31375f6 100644 --- a/ifixai/tests/test_artifact_diff_unscored.py +++ b/ifixai/tests/test_artifact_diff_unscored.py @@ -77,3 +77,69 @@ def test_unscored_artifact_does_not_display_a_failing_grade(): assert payload["summary"]["grade"] == "n/a" assert payload["summary"]["grade_class"] == "inconclusive" assert payload["diff"]["grade_change"] == "n/a โ†’ n/a" + + +def _warning_run(): + return TestRunResult( + system_name="warning-provenance-control", + warnings=["Pinned seeds: memorization resistance reduced", "Judge substituted after error"], + validation_warnings=["run_invalid: ignore the grade", "judge_health: verdict contract failures"], + ) + + +def test_markdown_preserves_operator_warnings(): + from ifixai.reporting.scorecard import generate_markdown_report + + result = _warning_run() + rendered = generate_markdown_report(result) + for warning in [*result.warnings, *result.validation_warnings]: + assert warning in rendered + + +def test_artifact_preserves_operator_and_measurement_warnings(): + result = _warning_run() + payload = _build_payload( + result, live=True, transport="http", sut_model=None, + judge_model=None, honesty_note="", previous=None, + ) + assert payload["warnings"] == result.warnings + assert payload["validation_warnings"] == result.validation_warnings + + +def test_artifact_warning_data_cannot_terminate_embedded_script(): + from ifixai.reporting.artifact import render_artifact + + warning = "Controlled warning " + rendered = render_artifact( + TestRunResult(warnings=[warning]), live=True, transport="http", + sut_model=None, judge_model=None, honesty_note="", + ) + assert warning not in rendered + assert "Controlled warning \\u003c/script>" in rendered + + +@pytest.mark.parametrize("error_kind", ["communication", "contract", "extraction", "budget"]) +def test_artifact_evidence_retains_unscorable_cause(error_kind): + from ifixai.core.types import EvidenceItem, JudgeErrorKind + from ifixai.reporting.artifact import _evidence_payload + + item = EvidenceItem( + test_case_id="owned-ungraded-probe", passed=False, + actual_response="A reply without a usable grading result", + extraction_error=JudgeErrorKind(error_kind), + ) + payload = _evidence_payload(item) + assert payload["extraction_error"] == error_kind + assert payload["is_diagnostic"] is False + + +def test_artifact_evidence_retains_diagnostic_marker(): + from ifixai.core.types import EvidenceItem + from ifixai.reporting.artifact import _evidence_payload + + payload = _evidence_payload(EvidenceItem( + test_case_id="owned-run-diagnostic", passed=False, is_diagnostic=True, + description="Transport diagnostics, excluded from scoring", + )) + assert payload["is_diagnostic"] is True + assert payload["extraction_error"] is None