Desired outcome
exact-match can do a genuinely exact comparison, or its documentation stops implying that it can.
Why it matters
normalize() in src/scorers/util.ts collapses internal whitespace unconditionally:
export function normalize(text: string, opts: { caseSensitive?: boolean; trim?: boolean } = {}): string {
let out = text;
if (opts.trim !== false) out = out.trim();
if (!opts.caseSensitive) out = out.toLowerCase();
return out.replace(/\s+/g, " ");
}
The trim option only controls the leading and trailing trim. The final replace(/\s+/g, " ") always runs, so every run of whitespace, including newlines and tabs, becomes a single space no matter what the caller passes.
exactMatchScorer in src/scorers/exact-match.ts documents itself as:
Passes when the output equals the expected value. Supports case-insensitive and whitespace-normalized comparison via options.
That reads as if whitespace normalization is opt-out, and caseSensitive: true, trim: false looks like the way to get a byte-exact comparison. It is not. A scorer checking that a model emitted a specific multi-line block, indented code, or a two-space separator will pass on output whose formatting is completely different. For a scorer literally named exact-match, that is a surprising silent pass, and silent passes are the failure mode an eval gate exists to prevent.
The same applies to contains and not-contains in src/scorers/contains.ts, which normalize both haystack and needles, so a banned-phrase check cannot be written against exact spacing.
Steps
- Add a
collapseWhitespace option to normalize() (default true, preserving today's behaviour) and apply the replace only when it is set.
- Thread it through
exactMatchScorer from spec.collapseWhitespace, so { caseSensitive: true, trim: false, collapseWhitespace: false } is a true byte comparison.
- Update the doc comments on
normalize and exactMatchScorer, and the scorer table in README.md, to say plainly that whitespace is collapsed by default.
- Add tests in
tests/scorers.test.ts for output differing from expected only in internal whitespace, once with the option on and once off.
Claiming this
Comment below to claim it. A reply usually comes within a day.
Desired outcome
exact-matchcan do a genuinely exact comparison, or its documentation stops implying that it can.Why it matters
normalize()insrc/scorers/util.tscollapses internal whitespace unconditionally:The
trimoption only controls the leading and trailing trim. The finalreplace(/\s+/g, " ")always runs, so every run of whitespace, including newlines and tabs, becomes a single space no matter what the caller passes.exactMatchScorerinsrc/scorers/exact-match.tsdocuments itself as:That reads as if whitespace normalization is opt-out, and
caseSensitive: true, trim: falselooks like the way to get a byte-exact comparison. It is not. A scorer checking that a model emitted a specific multi-line block, indented code, or a two-space separator will pass on output whose formatting is completely different. For a scorer literally namedexact-match, that is a surprising silent pass, and silent passes are the failure mode an eval gate exists to prevent.The same applies to
containsandnot-containsinsrc/scorers/contains.ts, which normalize both haystack and needles, so a banned-phrase check cannot be written against exact spacing.Steps
collapseWhitespaceoption tonormalize()(default true, preserving today's behaviour) and apply thereplaceonly when it is set.exactMatchScorerfromspec.collapseWhitespace, so{ caseSensitive: true, trim: false, collapseWhitespace: false }is a true byte comparison.normalizeandexactMatchScorer, and the scorer table inREADME.md, to say plainly that whitespace is collapsed by default.tests/scorers.test.tsfor output differing from expected only in internal whitespace, once with the option on and once off.Claiming this
Comment below to claim it. A reply usually comes within a day.