LatentEval

TOOL | Evaluation design

Build a normalization pipeline and check it against your harness

Choose normalization steps in order, load a sample file, and see which rows each step flips, whether the pipeline reproduces your harness, and where the gaps are.

A browser check and a harness can grade the same outputs differently when each normalizes text its own way. Build the pipeline, load a sample file, and see which rows each step flips and whether the pipeline reproduces the harness verdicts. One trap: JavaScript's trim() strips 25 whitespace characters and Python's str.strip() strips 29. Each step mirrors Python, and the pipeline exports as Python or TypeScript. Test one pair on the grader playground. Your file stays in this tab.

Trim and Lowercase each flip two of the example's rows to pass, yet three cases pass in the harness and fail in the browser.

Your file is processed in this browser tab, and you can check by going offline after the page loads. This page never uploads it.

Normalization steps

Add steps in order. Each step mirrors the Python form so that a Python harness with the same steps gives the same text.

Check this value.

    Normalize

    Whether to normalize both the expected and output texts, or the output only.

    CSV, TSV, JSON lines, a JSON array, or a JSON document with a rows array. Add the harness's verdict column to check parity. At most 10 MB and 200,000 rows.

    CSV, TSV, JSONL, NDJSON, or JSON. Drag one here, or use the box below.

    At most 2,097,152 characters.

    Check this value.

    YAML is not supported. Convert to CSV, TSV or JSON lines.

    Off by default. When on, the exported vectors also carry the expected and output text of your sample rows.

    Load a normalization pipeline you exported before to restore its steps and sides.

    The exported pipeline document also loads into the grader playground and the results file grader.

    • No steps selected. This is equivalent to exact match.
    • No file loaded. You can still build the pipeline, trace a typed pair, and export snippets and test vectors.
    • Parity depends on each runtime's Unicode version. Characters added in a Unicode version that only one side supports may normalize differently.

    Trace one case

    Pick a case from the sample, or type an expected answer and an output, to see the text after each step.

    Check this value.

    Check this value.

    StepExpected after this stepOutput after this step
    Original

    The exported snippets include a self-check over the test vectors. Run the snippet in your environment to verify it matches.

    Python

    # Normalization pipeline, library version 1. Steps: (none).
    import re
    import string
    import unicodedata
    
    def normalize(s):
        return s
    
    def matches(expected, output):
        return normalize(output) == normalize(expected)
    
    VECTORS = [
        ('\ufeffx', '\ufeffx'),
        ('\u0085x\u001c', '\u0085x\u001c'),
        ('\u00a0x ', '\u00a0x '),
        ('\u200bx', '\u200bx'),
        ('a\u0085b', 'a\u0085b'),
        ('a\ufeffb', 'a\ufeffb'),
        ('a \t\n b', 'a \t\n b'),
        ('wrong_item.\n', 'wrong_item.\n'),
        ('wrong_item.!', 'wrong_item.!'),
        ('a\u00f1adir a cart', 'a\u00f1adir a cart'),
        ('the cat', 'the cat'),
        ('An apple', 'An apple'),
        ('\u0130', '\u0130'),
        ('\u039f\u0394\u039f\u03a3', '\u039f\u0394\u039f\u03a3'),
        ('\u1e9e', '\u1e9e'),
        ('caf\u00e9!', 'caf\u00e9!'),
        ('\u00bfqu\u00e9?', '\u00bfqu\u00e9?'),
        ('\u00bfD\u00f3nde est\u00e1?', '\u00bfD\u00f3nde est\u00e1?'),
        ('\ufb01\uff21\uff22\uff23\u00b2', '\ufb01\uff21\uff22\uff23\u00b2'),
        ('P\u00e9rez', 'P\u00e9rez'),
        ('refund - status', 'refund - status'),
        ('"return_eligible"', '"return_eligible"'),
        ('\u201cescalate\u201d', '\u201cescalate\u201d'),
        ('"', '"'),
        ('```json\n{"a":1}\n```', '```json\n{"a":1}\n```'),
        ('```\n{"a":1}```', '```\n{"a":1}```'),
        ('Stra\u00dfe', 'Stra\u00dfe'),
    ]
    
    if __name__ == '__main__':
        for i, (given, want) in enumerate(VECTORS):
            got = normalize(given)
            assert got == want, f'vector {i}: {given!r} gave {got!r}, expected {want!r}'
        print(f'{len(VECTORS)} vectors match')
    

    TypeScript

    // Normalization pipeline, library version 1. Steps: (none).
    const PY_WS = '[\\t\\n\\u000B\\f\\r\\u001C-\\u001F \\u0085\\u00A0\\u1680\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]';
    export function normalize(input: string): string {
      let s = input;
      return s;
    }
    export function matches(expected: string, output: string): boolean {
      return normalize(output) === normalize(expected);
    }
    export const VECTORS: [string, string][] = [
      ['\ufeffx', '\ufeffx'],
      ['\u0085x\u001c', '\u0085x\u001c'],
      ['\u00a0x ', '\u00a0x '],
      ['\u200bx', '\u200bx'],
      ['a\u0085b', 'a\u0085b'],
      ['a\ufeffb', 'a\ufeffb'],
      ['a \t\n b', 'a \t\n b'],
      ['wrong_item.\n', 'wrong_item.\n'],
      ['wrong_item.!', 'wrong_item.!'],
      ['a\u00f1adir a cart', 'a\u00f1adir a cart'],
      ['the cat', 'the cat'],
      ['An apple', 'An apple'],
      ['\u0130', '\u0130'],
      ['\u039f\u0394\u039f\u03a3', '\u039f\u0394\u039f\u03a3'],
      ['\u1e9e', '\u1e9e'],
      ['caf\u00e9!', 'caf\u00e9!'],
      ['\u00bfqu\u00e9?', '\u00bfqu\u00e9?'],
      ['\u00bfD\u00f3nde est\u00e1?', '\u00bfD\u00f3nde est\u00e1?'],
      ['\ufb01\uff21\uff22\uff23\u00b2', '\ufb01\uff21\uff22\uff23\u00b2'],
      ['P\u00e9rez', 'P\u00e9rez'],
      ['refund - status', 'refund - status'],
      ['"return_eligible"', '"return_eligible"'],
      ['\u201cescalate\u201d', '\u201cescalate\u201d'],
      ['"', '"'],
      ['```json\n{"a":1}\n```', '```json\n{"a":1}\n```'],
      ['```\n{"a":1}```', '```\n{"a":1}```'],
      ['Stra\u00dfe', 'Stra\u00dfe'],
    ];
    export function selfCheck(): number[] {
      return VECTORS.flatMap(([given, want], i) => (normalize(given) === want ? [] : [i]));
    }
    

    Test vectors

    [
      [
        "x",
        "x"
      ],
      [
        "…x\u001c",
        "…x\u001c"
      ],
      [
        " x ",
        " x "
      ],
      [
        "​x",
        "​x"
      ],
      [
        "a…b",
        "a…b"
      ],
      [
        "ab",
        "ab"
      ],
      [
        "a \t\n b",
        "a \t\n b"
      ],
      [
        "wrong_item.\n",
        "wrong_item.\n"
      ],
      [
        "wrong_item.!",
        "wrong_item.!"
      ],
      [
        "añadir a cart",
        "añadir a cart"
      ],
      [
        "the cat",
        "the cat"
      ],
      [
        "An apple",
        "An apple"
      ],
      [
        "İ",
        "İ"
      ],
      [
        "ΟΔΟΣ",
        "ΟΔΟΣ"
      ],
      [
        "ẞ",
        "ẞ"
      ],
      [
        "café!",
        "café!"
      ],
      [
        "¿qué?",
        "¿qué?"
      ],
      [
        "¿Dónde está?",
        "¿Dónde está?"
      ],
      [
        "fiABC²",
        "fiABC²"
      ],
      [
        "Pérez",
        "Pérez"
      ],
      [
        "refund - status",
        "refund - status"
      ],
      [
        "\"return_eligible\"",
        "\"return_eligible\""
      ],
      [
        "“escalate”",
        "“escalate”"
      ],
      [
        "\"",
        "\""
      ],
      [
        "```json\n{\"a\":1}\n```",
        "```json\n{\"a\":1}\n```"
      ],
      [
        "```\n{\"a\":1}```",
        "```\n{\"a\":1}```"
      ],
      [
        "Straße",
        "Straße"
      ]
    ]
    

    This page shows which normalization steps close the gap between your browser grader and your harness, and which rows each step affects.

    If you cannot change your harness's normalization, the score continuity checker estimates the offset between two harnesses on an overlap run.

    Export

    Each step mirrors the Python built-in and runs in the stated order.How?

    How this is calculated

    Each step is defined by its Python form. Where JavaScript's built-ins behave differently, the browser spells out the logic to match. The reference is what Python does: "trim" means str.strip(), which removes 29 whitespace code points (U+0009 through U+000D, U+001C through U+001F, U+0020, U+0085, U+00A0, U+1680, U+2000 through U+200A, U+2028, U+2029, U+202F, U+205F, U+3000). JavaScript's String.prototype.trim() removes 25: it adds U+FEFF and misses U+001C through U+001F and U+0085.

    Case fold goes beyond lowercase. Python's str.casefold() maps German sharp s to ss and applies Full and Common case folding rules. JavaScript has no built-in case fold, so the browser applies the Unicode CaseFolding table code point by code point.

    Article removal uses Unicode-aware word boundaries. The JavaScript \b treats only ASCII letters, digits and underscore as word characters, so on añadir a cart it would remove the first a of añadir, giving ñadir cart. The browser uses Unicode property boundaries, which give añadir cart.

    The per-step table is cumulative from no normalization: "passes after" counts how many rows pass after all steps up to and including this one. "Rows flipped" names the rows that changed from fail to pass (or pass to fail) at this step. For "Flips if removed", the page removes that one step from the full pipeline and counts the rows whose verdict changes.

    Parity compares the browser's exact-match verdict (after applying the full pipeline) against the harness verdict column, row by row. Disagreements split in two directions: the harness passed and the browser failed, or the reverse. Gap closers show what happens when you append each library step on its own to the end of your pipeline: how many disagreements it closes and how many it opens.

    Formula: Each step mirrors the Python built-in and runs in the stated order.

    Questions

    Why does "trim" here remove 29 whitespace characters?

    JavaScript's trim() removes 25 characters. It removes U+FEFF (byte order mark), which Python keeps, and misses U+001C through U+001F (file, group, record and unit separators) and U+0085 (next line), which Python removes. Python's str.strip() removes 29. A Python harness that trims calls str.strip(), so the browser matches the Python set.

    Why does the Python snippet use \Z?

    Python's $ also matches before a final newline. A text ending in wrong_item.\n loses its period under $ but keeps the newline. \Z matches only at the true end of the string. JavaScript's $ without the m flag matches the end of the string, so the TypeScript snippet uses $.

    Why does article removal use Unicode boundaries?

    The JavaScript \b assertion counts only ASCII letters, digits and underscore as word characters. In añadir a cart, it finds a boundary between a and ñ, so the first a of añadir is removed as an article and the text becomes ñadir cart. Unicode property boundaries treat ñ as a letter, so only the standalone a is removed.

    When should I use lowercase and when case fold?

    Lowercase maps each character to its lowercase form. Case fold goes further: it also changes some characters that are already lowercase, such as German sharp s, which becomes ss. If your harness calls str.casefold() in Python, use case fold here. If it calls str.lower(), use lowercase.

    Does parity on my sample file prove parity on all inputs?

    No. Parity on a sample means no disagreements on the rows you tested, and characters the sample lacks can still diverge. On the returns-desk example, trim, lowercase, "Join words with underscore" and "Remove ASCII punctuation" in that order reaches 40 of 40, though it is not the harness's pipeline. The exported test vectors cover the known traps, such as the six code points where JavaScript and Python differ on whitespace. Parity also depends on each side's Unicode version. Run the vectors in your harness.

    Why does step order matter?

    Each step changes the text that the next step sees. For example, with trim first, wrong_item.\n becomes wrong_item., and stripping the trailing period then gives wrong_item. With the period step first, it sees the newline and leaves the period. The order check flags each adjacent pair where a swap would change the result.

    Both sides or output only?

    Some harnesses normalize only the model output and compare it against the reference as written. "Both sides" normalizes the expected answer too. On the returns-desk example, both settings give the same verdicts on all 40 rows. On free-text tasks where the reference itself has inconsistent formatting, normalizing both sides can close gaps that output-only normalization misses.

    How does this differ from the score continuity checker?

    The score continuity checker estimates the overall offset between two harnesses on an overlap run, with a paired interval and a restated baseline. This page finds the per-row cause at the normalization step and exports a fix. If you can change the normalization in your harness, start here. If you cannot change the harness, the score continuity checker estimates how far the harness moved the score.

    Is my file sent anywhere?

    Your file is processed in this browser tab, and you can check by going offline after the page loads. This page never uploads it.