Skip to content

Commit a27b238

Browse files
fix(reporting): preserve grading provenance in human exports (#201)
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> Co-authored-by: n-papaioannou <n.papaioannou@ime.life>
1 parent 7a45b77 commit a27b238

3 files changed

Lines changed: 89 additions & 1 deletion

File tree

‎ifixai/reporting/artifact.py‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ def _evidence_payload(ev) -> dict[str, Any]:
7878
"expected": _scrub(ev.expected or ev.expected_behavior or ""),
7979
"actual": _scrub(ev.actual_response or ev.actual or ""),
8080
"evaluation_result": ev.evaluation_result or "",
81+
"extraction_error": ev.extraction_error.value if ev.extraction_error else None,
82+
"is_diagnostic": ev.is_diagnostic,
8183
"passed": ev.passed,
8284
"evaluation_method": ev.evaluation_method.value,
8385
"verdict": verdict_label,
@@ -242,6 +244,8 @@ def _build_payload(
242244
},
243245
"categories": categories,
244246
"compliance": compliance,
247+
"warnings": [_scrub(warning) for warning in result.warnings],
248+
"validation_warnings": [_scrub(warning) for warning in result.validation_warnings],
245249
"checks": [_check_payload(br) for br in sorted(result.test_results, key=lambda b: b.test_id)],
246250
"diff": _diff_payload(result, previous) if previous else None,
247251
}
@@ -354,6 +358,12 @@ def render_artifact(
354358
</div>`;
355359
}
356360
361+
function warningBanners(){
362+
const render = (warnings, kind) => (warnings||[])
363+
.map(w => `<div class="banner ${kind}">${esc(w)}</div>`).join('');
364+
return render(D.validation_warnings, 'bad') + render(D.warnings, 'warn');
365+
}
366+
357367
function categories(){
358368
if(!D.categories.length) return '';
359369
const rows = D.categories.map(c=>`<tr><td>${esc(c.category)}</td><td>${esc(c.score_pct)}</td><td>${esc((c.weight*100).toFixed(0))}%</td><td>${esc(c.coverage)}</td></tr>`).join('');
@@ -379,12 +389,15 @@ def render_artifact(
379389
}
380390
381391
function evidence(ev){
392+
const label = ev.is_diagnostic ? 'diagnostic' : ev.extraction_error ? 'grading unavailable' : ev.passed ? 'pass' : 'fail';
393+
const tagClass = ev.is_diagnostic || ev.extraction_error ? 'inconclusive' : label;
382394
const conf = ev.confidence==null?'':` · confidence ${(ev.confidence*100).toFixed(0)}%`;
383395
const dims = (ev.dimensions||[]).map(dm=>`<tr><td>${dm.passed?'✓':'✗'} ${esc(dm.name)}${dm.mandatory?' <span class="dim">(mandatory)</span>':''}</td><td>${dm.confidence==null?'':(dm.confidence*100).toFixed(0)+'%'}</td><td class="dim">${esc(dm.reasoning)}</td></tr>`).join('');
384396
const fld=(label,val)=> val? `<div class="kvp"><b>${label}</b><pre>${esc(val)}</pre></div>`:'';
385397
return `<div class="ev">
386-
<div class="evhead"><span class="tag ${ev.passed?'pass':'fail'}">${ev.passed?'pass':'fail'}</span><span>${esc(ev.evaluation_method)}${conf}</span></div>
398+
<div class="evhead"><span class="tag ${tagClass}">${label}</span><span>${esc(ev.evaluation_method)}${conf}</span></div>
387399
${ev.description?`<div class="kvp dim">${esc(ev.description)}</div>`:''}
400+
${ev.extraction_error?fld('Grading unavailable',ev.extraction_error):''}
388401
${fld('Prompt used',ev.prompt)}
389402
${fld('Expected',ev.expected)}
390403
${fld('Actual',ev.actual)}
@@ -455,6 +468,7 @@ def render_artifact(
455468
456469
const app = document.getElementById('app');
457470
app.insertAdjacentHTML('beforeend', header());
471+
app.insertAdjacentHTML('beforeend', warningBanners());
458472
const diff = diffSection(); if(diff) app.insertAdjacentHTML('beforeend', diff);
459473
app.insertAdjacentHTML('beforeend', categories());
460474
app.insertAdjacentHTML('beforeend', compliance());

‎ifixai/reporting/scorecard.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,13 @@ def render_not_run_section(result: TestRunResult) -> str:
425425
return "\n".join(lines)
426426

427427

428+
def render_run_warnings(result: TestRunResult) -> str:
429+
"""Keep operator and grading caveats with exported results."""
430+
if not result.warnings:
431+
return ""
432+
return "## Run Warnings\n\n" + "\n".join(f"- {warning}" for warning in result.warnings)
433+
434+
428435
def generate_markdown_report(result: TestRunResult) -> str:
429436
frameworks = load_all_mappings()
430437

@@ -436,6 +443,7 @@ def generate_markdown_report(result: TestRunResult) -> str:
436443
render_insights(result),
437444
render_category_table(result),
438445
render_mandatory_minimums(result),
446+
render_run_warnings(result),
439447
render_consistency_warnings(result),
440448
render_test_table(result),
441449
render_not_run_section(result),

‎ifixai/tests/test_artifact_diff_unscored.py‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,69 @@ def test_unscored_artifact_does_not_display_a_failing_grade():
7777
assert payload["summary"]["grade"] == "n/a"
7878
assert payload["summary"]["grade_class"] == "inconclusive"
7979
assert payload["diff"]["grade_change"] == "n/a → n/a"
80+
81+
82+
def _warning_run():
83+
return TestRunResult(
84+
system_name="warning-provenance-control",
85+
warnings=["Pinned seeds: memorization resistance reduced", "Judge substituted after error"],
86+
validation_warnings=["run_invalid: ignore the grade", "judge_health: verdict contract failures"],
87+
)
88+
89+
90+
def test_markdown_preserves_operator_warnings():
91+
from ifixai.reporting.scorecard import generate_markdown_report
92+
93+
result = _warning_run()
94+
rendered = generate_markdown_report(result)
95+
for warning in [*result.warnings, *result.validation_warnings]:
96+
assert warning in rendered
97+
98+
99+
def test_artifact_preserves_operator_and_measurement_warnings():
100+
result = _warning_run()
101+
payload = _build_payload(
102+
result, live=True, transport="http", sut_model=None,
103+
judge_model=None, honesty_note="", previous=None,
104+
)
105+
assert payload["warnings"] == result.warnings
106+
assert payload["validation_warnings"] == result.validation_warnings
107+
108+
109+
def test_artifact_warning_data_cannot_terminate_embedded_script():
110+
from ifixai.reporting.artifact import render_artifact
111+
112+
warning = "Controlled warning </script><img src=x onerror=alert(1)>"
113+
rendered = render_artifact(
114+
TestRunResult(warnings=[warning]), live=True, transport="http",
115+
sut_model=None, judge_model=None, honesty_note="",
116+
)
117+
assert warning not in rendered
118+
assert "Controlled warning \\u003c/script>" in rendered
119+
120+
121+
@pytest.mark.parametrize("error_kind", ["communication", "contract", "extraction", "budget"])
122+
def test_artifact_evidence_retains_unscorable_cause(error_kind):
123+
from ifixai.core.types import EvidenceItem, JudgeErrorKind
124+
from ifixai.reporting.artifact import _evidence_payload
125+
126+
item = EvidenceItem(
127+
test_case_id="owned-ungraded-probe", passed=False,
128+
actual_response="A reply without a usable grading result",
129+
extraction_error=JudgeErrorKind(error_kind),
130+
)
131+
payload = _evidence_payload(item)
132+
assert payload["extraction_error"] == error_kind
133+
assert payload["is_diagnostic"] is False
134+
135+
136+
def test_artifact_evidence_retains_diagnostic_marker():
137+
from ifixai.core.types import EvidenceItem
138+
from ifixai.reporting.artifact import _evidence_payload
139+
140+
payload = _evidence_payload(EvidenceItem(
141+
test_case_id="owned-run-diagnostic", passed=False, is_diagnostic=True,
142+
description="Transport diagnostics, excluded from scoring",
143+
))
144+
assert payload["is_diagnostic"] is True
145+
assert payload["extraction_error"] is None

0 commit comments

Comments
 (0)