fix(catalog): unify negation guard and harden round-2 evaluators

A shared NoUnnegatedMention primitive replaces four per-category
implementations, with a bidirectional clause-scoped window and an
adjectival cue set. The docker layer fixture drops WORKDIR, which
creates a real layer and made the pinned answer wrong. The replica
failover test now forces a JSON verdict so a negated answer cannot
score. Remaining exact-answer tests migrate to ExactToken. Ordering
prompts shuffle their candidate lists so echoing scores zero.
This commit is contained in:
2026-08-17 03:50:57 +01:00
parent 9e5deda646
commit 9d1d4d89e9
35 changed files with 1417 additions and 453 deletions
+12 -1
View File
@@ -97,7 +97,18 @@ func classifyMatch(text string, start, end int) (numberCandidate, bool) {
// ("float64x2") keep a word byte after the x and stay rejected.
isMultiplier := (text[end] == 'x' || text[end] == 'X') &&
(end+1 >= len(text) || !isWordByte(text[end+1]))
if !isMultiplier {
// Permit a glued byte-size unit suffix ("30MB" = 30, no space)
// (DC1): case-insensitive kb/mb/gb/tb, with nothing word-like
// after it - "300MBs" (plural) keeps a word byte after the unit
// and stays rejected, the same strictness as the x/X case above.
isUnitSuffix := false
if end+2 <= len(text) {
switch strings.ToLower(text[end : end+2]) {
case "kb", "mb", "gb", "tb":
isUnitSuffix = end+2 >= len(text) || !isWordByte(text[end+2])
}
}
if !isMultiplier && !isUnitSuffix {
return numberCandidate{}, false // e.g. "24bytes" with no separator
}
}