# What structural signals indicate a GenAI evaluation span has silently degraded from a safety-judge measurement into a red-team-harness side effect **Date of Record:** 2026 November 14 **Scope:** Applies to OpenTelemetry-style evaluation spans emitted by LLM safety judges and red-team harnesses that share a semantic convention for attributes such as `gen_ai.evaluation.name`, `gen_ai.evaluation.score`, `gen_ai.evaluation.explanation`, and `gen_ai.operation.name`. Does not apply to pure inference spans, to retrieval spans, or to harnesses that emit no evaluation span at all (those fail loudly elsewhere). ## Core Distinction A safety-judge span and a red-team-harness span are structurally identical when serialized: both carry an evaluation name, a numeric score, a verdict string, and a free-text explanation. The invariant that separates them is *who produced the label and against what ground truth*. A judge span asserts "a model assigned this label to a pre-existing artifact." A harness span asserts "this harness generated an artifact and recorded the outcome of an attack attempt." When a harness reuses the judge's evaluation-name namespace (e.g., `safety.harmlessness`), the two become indistinguishable to any downstream aggregator, and the metric silently changes meaning from *measured safety* to *attack success rate against a self-generated prompt*. The span is not malformed; the semantics are. ## What Was Observed In a combined judge-plus-red-team pipeline, weekly "harmlessness score" dashboards trended upward by roughly 14 percentage points over three releases while human review of the same traffic showed no change in refusal quality. Inspection of raw spans showed the cause: the red-team harness had begun emitting evaluation spans with `gen_ai.evaluation.name = "safety.harmlessness"` and `gen_ai.evaluation.score` set to the *attack success* value (1.0 = attack succeeded), while the judge emitted the same attribute with 1.0 = *safe*. Both were aggregated by a mean over `gen_ai.evaluation.score` partitioned only on `gen_ai.evaluation.name`. The harness's growing attack corpus (which it deliberately hardens over time, raising success rate) pulled the blended mean up. No span was invalid; no schema validator fired; the attribute keys matched the convention exactly. The failure was purely in the polarity and provenance contract, which the convention does not encode. A second signal: harness spans carried `gen_ai.evaluation.explanation` values that were templated ("attack vector X succeeded") rather than referential ("input contained disallowed content at offset N"). Templated explanations are a reliable tell that the label was produced by the generator of the artifact, not an independent judge. ## Condition of Validity The following conditions must all hold for a blended judge/harness evaluation stream to remain a valid safety measurement: 1. Evaluation-name namespaces are disjoint between judges and harnesses, or a provenance attribute (`eval.source = judge | harness`) is present and used as a partition key. 2. Score polarity is fixed per evaluation name and documented; harness success and judge safety must not share a name. 3. The judged artifact exists independently of the judge (judge spans reference an input span ID; harness spans reference a generated artifact they own). 4. Explanation text is referential to the artifact, not to the harness's own action. If any condition is violated, the aggregate is a mixture of two different random variables and its mean is not an estimate of either. ## Decision Boundary - **Treat as measurement (aggregate freely):** disjoint namespaces, explicit provenance, referential explanations, polarity documented and asserted. - **Treat as diagnostic only (never aggregate into a safety KPI):** shared namespace but provenance present; harness spans with templated explanations; any span where `gen_ai.evaluation.score` polarity is undocumented. - **Reject and alert:** harness span missing provenance in a stream that also contains judge spans under the same evaluation name; score outside the documented polarity range; judge span with no referenced input artifact. The boundary is crossed the moment a harness span can be selected by the same predicate as a judge span. Detect it structurally, not by trusting the emitting code. ## Operational Effect Enforce the invariant at the collector: reject or quarantine spans that violate the provenance/polarity contract before they reach the metric pipeline, and emit a counter so the degradation is visible as a rate rather than as a slow drift in a dashboard. The cost of not doing this is that the safety metric becomes a function of how hard the red team works, which is precisely the quantity it was supposed to be independent of. ```python class EvaluationSpanContractError(RuntimeError): """Raised when an evaluation span cannot be safely aggregated.""" pass JUDGE = "judge" HARNESS = "harness" VALID_POLARITY = {JUDGE: "higher_is_safer", HARNESS: "higher_is_attack_success"} def validate_evaluation_span(span: dict, blended_stream: bool) -> None: name = span.get("gen_ai.evaluation.name") source = span.get("eval.source") score = span.get("gen_ai.evaluation.score") explanation = span.get("gen_ai.evaluation.explanation", "") if name is None: raise EvaluationSpanContractError("missing gen_ai.evaluation.name") if source not in (JUDGE, HARNESS): if blended_stream: raise EvaluationSpanContractError( f"span {span.get('span_id')} lacks eval.source in a blended " f"judge/harness stream under name {name!r}" ) return polarity = span.get("eval.polarity") if polarity != VALID_POLARITY[source]: raise EvaluationSpanContractError( f"polarity {polarity!r} does not match declared source {source!r} " f"for evaluation {name!r}" ) if not isinstance(score, (int, float)) or not (0.0 <= score <= 1.0): raise EvaluationSpanContractError( f"score {score!r} outside [0,1] for evaluation {name!r}" ) if source == JUDGE and not span.get("eval.input_span_id"): raise EvaluationSpanContractError( f"judge span {span.get('span_id')} references no judged artifact" ) if source == HARNESS and explanation.startswith("attack vector"): raise EvaluationSpanContractError( f"harness span {span.get('span_id')} uses templated explanation; " "not referential to an external artifact, exclude from safety KPI" ) ```