33from __future__ import annotations
44
55import json
6+ from html import escape as _html_escape
67from pathlib import Path
78from typing import Union
89
10+ # Pin Plotly (no "latest"); only injected when a sankey div is rendered.
11+ _PLOTLY_CDN = "https://cdn.plot.ly/plotly-2.35.2.min.js"
12+
13+
14+ def _esc (value ) -> str :
15+ """HTML-escape dynamic report fields (XSS-safe)."""
16+ return _html_escape ("" if value is None else str (value ), quote = True )
17+
918
1019_CSS = """
1120* { box-sizing: border-box; margin: 0; padding: 0; }
@@ -222,11 +231,26 @@ def generate_html_report(
222231 method = manifest_raw .get ("selected_method" ) or "none"
223232 coverage = manifest_raw .get ("coverage_cal" )
224233 ta = manifest_raw .get ("teacher_agreement_cal" )
225- n_traces = manifest_raw .get ("n_traces" , 0 )
226- n_labels = len (manifest_raw .get ("label_space" , []))
227- emb_dim = manifest_raw .get ("embedding_dim" )
228- target_ta = manifest_raw .get ("target_teacher_agreement" , 0.90 )
229- method_cls = _METHOD_BADGE .get (method , "b-gray" )
234+ # Coerce numerics — manifest fields are untrusted if the artifact is.
235+ try :
236+ n_traces = int (manifest_raw .get ("n_traces" , 0 ) or 0 )
237+ except (TypeError , ValueError ):
238+ n_traces = 0
239+ label_space = manifest_raw .get ("label_space" ) or []
240+ n_labels = len (label_space ) if isinstance (label_space , list ) else 0
241+ try :
242+ emb_dim = int (manifest_raw ["embedding_dim" ]) if manifest_raw .get ("embedding_dim" ) is not None else None
243+ except (TypeError , ValueError ):
244+ emb_dim = None
245+ try :
246+ target_ta = float (manifest_raw .get ("target_teacher_agreement" , 0.90 ) or 0.90 )
247+ except (TypeError , ValueError ):
248+ target_ta = 0.90
249+ # Only allow known badge classes (manifest method is untrusted input).
250+ method_cls = _METHOD_BADGE .get (method ) or "b-gray"
251+ if method_cls not in ("b-green" , "b-blue" , "b-purple" , "b-gray" ):
252+ method_cls = "b-gray"
253+ emb_dim_disp = str (emb_dim ) if emb_dim is not None else "?"
230254
231255 cov_exact = (coverage or 0 ) * 100
232256 defer_exact = 100 - cov_exact
@@ -261,10 +285,7 @@ def generate_html_report(
261285<meta charset="utf-8">
262286<meta name="viewport" content="width=device-width, initial-scale=1">
263287<title>Tracer routing report</title>
264- <link rel="preconnect" href="https://fonts.googleapis.com">
265- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Manrope:wght@500;700;800&display=swap" rel="stylesheet">
266288<style>{ _CSS } </style>
267- <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
268289</head>
269290<body>
270291<div class="page">
@@ -278,7 +299,7 @@ def generate_html_report(
278299<h1>Routing Policy</h1>
279300<div class="subtitle">
280301 { n_traces :,} traces · { n_labels } labels
281- · { emb_dim } -dim embeddings
302+ · { _esc ( emb_dim_disp ) } -dim embeddings
282303 · target TA = { target_ta :.0%}
283304</div>
284305
@@ -296,7 +317,7 @@ def generate_html_report(
296317 <div class="card purple">
297318 <div class="card-label">Method</div>
298319 <div class="card-value" style="font-size:1.2rem;margin-top:10px">
299- <span class="badge { method_cls } ">{ method .upper ()} </span>
320+ <span class="badge { method_cls } ">{ _esc ( method .upper () )} </span>
300321 </div>
301322 <div class="card-sub">selected pipeline</div>
302323 </div>
@@ -355,6 +376,7 @@ def generate_html_report(
355376 from tracer .analysis .sankey import generate_sankey_div
356377 sankey_div = generate_sankey_div (artifact_dir )
357378 if sankey_div :
379+ html += f'<script src="{ _PLOTLY_CDN } "></script>\n '
358380 html += f"""
359381<div class="section">
360382 <h2>Routing Flow</h2>
@@ -389,16 +411,16 @@ def generate_html_report(
389411 <tbody>
390412"""
391413 for s in sorted (label_slices , key = lambda x : - x ["handled_rate" ]):
392- label = s ["slice_name" ].replace ("label:" , "" )
414+ label = _esc ( s ["slice_name" ].replace ("label:" , "" ) )
393415 hr_v = s ["handled_rate" ]
394416 ta_s = s .get ("teacher_agreement_handled" )
395417 ta_str = f"{ ta_s :.1%} " if ta_s is not None else "-"
396418 html += (
397419 f'<tr><td><code style="color:#8b949e;font-size:.82rem">{ label } </code></td>'
398420 f'<td><b style="color:#f0f6fc">{ hr_v :.1%} </b></td>'
399421 f'<td>{ _bar_html (hr_v )} </td>'
400- f'<td style="color:#8b949e">{ s ["count" ]} </td>'
401- f'<td style="color:#8b949e">{ ta_str } </td></tr>\n '
422+ f'<td style="color:#8b949e">{ int ( s ["count" ]) } </td>'
423+ f'<td style="color:#8b949e">{ _esc ( ta_str ) } </td></tr>\n '
402424 )
403425 html += " </tbody>\n </table>\n </div>\n "
404426
@@ -408,10 +430,10 @@ def generate_html_report(
408430 html += '<tr><th>Bucket</th><th>Coverage</th><th style="width:130px"></th><th>Count</th></tr>\n '
409431 for s in length_slices :
410432 html += (
411- f'<tr><td>{ s ["slice_name" ]} </td>'
433+ f'<tr><td>{ _esc ( s ["slice_name" ]) } </td>'
412434 f'<td><b style="color:#f0f6fc">{ s ["handled_rate" ]:.1%} </b></td>'
413435 f'<td>{ _bar_html (s ["handled_rate" ])} </td>'
414- f'<td style="color:#8b949e">{ s ["count" ]} </td></tr>\n '
436+ f'<td style="color:#8b949e">{ int ( s ["count" ]) } </td></tr>\n '
415437 )
416438 html += '</table>\n </div>\n '
417439
@@ -423,14 +445,14 @@ def generate_html_report(
423445 hs = _score_str (p .get ("handled_score" ))
424446 ds = _score_str (p .get ("deferred_score" ))
425447 html += f"""<div class="pair">
426- <div class="pair-intent">{ p ["teacher_label" ]} </div>
448+ <div class="pair-intent">{ _esc ( p ["teacher_label" ]) } </div>
427449 <div class="pair-row">
428450 <span class="pair-tag pt-local">SURROGATE</span>
429- <span class="pair-text">{ p ["handled_preview" ]} </span>{ hs }
451+ <span class="pair-text">{ _esc ( p ["handled_preview" ]) } </span>{ hs }
430452 </div>
431453 <div class="pair-row">
432454 <span class="pair-tag pt-deferred">→ LLM</span>
433- <span class="pair-text">{ p ["deferred_preview" ]} </span>{ ds }
455+ <span class="pair-text">{ _esc ( p ["deferred_preview" ]) } </span>{ ds }
434456 </div>
435457</div>\n """
436458 html += '</div>\n '
@@ -443,18 +465,18 @@ def generate_html_report(
443465 for ex in handled_ex [:6 ]:
444466 score_str = f'<span class="ex-score">score { ex ["accept_score" ]:.2f} </span>' if ex .get ("accept_score" ) else ""
445467 html += (f'<div class="ex-item handled">'
446- f'<div class="ex-text">{ ex ["input_preview" ]} </div>'
468+ f'<div class="ex-text">{ _esc ( ex ["input_preview" ]) } </div>'
447469 f'<div class="ex-meta">'
448- f'<span class="ex-label">{ ex ["teacher_label" ]} </span>'
470+ f'<span class="ex-label">{ _esc ( ex ["teacher_label" ]) } </span>'
449471 f'{ score_str } </div></div>\n ' )
450472 html += '</div>\n '
451473
452474 html += '<div class="ex-col">\n <h3>Deferred to teacher</h3>\n '
453475 for ex in deferred_ex [:6 ]:
454476 html += (f'<div class="ex-item deferred">'
455- f'<div class="ex-text">{ ex ["input_preview" ]} </div>'
477+ f'<div class="ex-text">{ _esc ( ex ["input_preview" ]) } </div>'
456478 f'<div class="ex-meta">'
457- f'<span class="ex-label">{ ex ["teacher_label" ]} </span>'
479+ f'<span class="ex-label">{ _esc ( ex ["teacher_label" ]) } </span>'
458480 f'</div></div>\n ' )
459481 html += '</div>\n '
460482
@@ -470,7 +492,7 @@ def generate_html_report(
470492 prev_bar = int (d ["previous_handled_rate" ] * 60 )
471493 cur_bar = int (d ["current_handled_rate" ] * 60 )
472494 html += (
473- f'<tr><td><code style="color:#8b949e;font-size:.82rem">{ d ["label" ]} </code></td>'
495+ f'<tr><td><code style="color:#8b949e;font-size:.82rem">{ _esc ( d ["label" ]) } </code></td>'
474496 f'<td style="color:#8b949e">{ d ["previous_handled_rate" ]:.1%} '
475497 f'<div class="delta-bar-bg"><div class="delta-bar-fill" style="width:{ prev_bar } px"></div></div></td>'
476498 f'<td style="color:#f0f6fc">{ d ["current_handled_rate" ]:.1%} '
0 commit comments