(kase, res)
| 133 | |
| 134 | // verdict.v ∈ correct | partial | wrong | unsupported | unevaluated | timeout | error |
| 135 | function classify(kase, res) { |
| 136 | if (!res) return { v: 'unsupported' }; |
| 137 | if (res.status === 'timeout') return { v: 'timeout' }; |
| 138 | if (res.status === 'unsupported') return { v: 'unsupported', note: res.reason }; |
| 139 | if (res.status === 'error') return { v: 'error', note: res.error }; |
| 140 | if (res.status === 'unevaluated') return { v: 'unevaluated' }; |
| 141 | if (res.status === 'overflow') return { v: 'wrong', note: 'overflow' }; |
| 142 | |
| 143 | const vr = kase.verify; |
| 144 | if (vr.kind === 'decimal') { |
| 145 | const m = matchingSigDigits(res.valueText, vr.value); |
| 146 | // -3 absorbs display rounding (a value can print a few fewer digits than |
| 147 | // requested when a carry collapses a run of trailing 9s, e.g. e). |
| 148 | if (m >= vr.sigdigits - 3) return { v: 'correct', note: `${Math.min(m, vr.sigdigits)} digits` }; |
| 149 | if (m >= 14) return { v: 'partial', note: `~${m} digits (double)` }; |
| 150 | return { v: 'wrong', note: `${m} digits` }; |
| 151 | } |
| 152 | if (vr.kind === 'integer') { |
| 153 | const got = normalizeInt(res.valueText); |
| 154 | return got === vr.value ? { v: 'correct', note: 'exact' } : { v: 'wrong', note: 'inexact' }; |
| 155 | } |
| 156 | if (vr.kind === 'sample') { |
| 157 | if (!allClose(res.values, vr.values)) return { v: 'wrong', note: 'value mismatch' }; |
| 158 | if (kase.category === 'simplify') { |
| 159 | // A decimal-float result is a numeric evaluation, not a symbolic |
| 160 | // simplification (e.g. √(3+2√2) -> 2.414… instead of 1+√2). Exact |
| 161 | // results like `0`, `1`, `4x` are fine — only flag a many-digit decimal. |
| 162 | if (/^[+-]?\d+\.\d{4,}(e[+-]?\d+)?$/i.test(norm(res.text))) |
| 163 | return { v: 'partial', note: 'numeric, not symbolic' }; |
| 164 | const changed = norm(res.text) !== norm(res.inputText); |
| 165 | return changed ? { v: 'correct' } : { v: 'partial', note: 'value ok, not simplified' }; |
| 166 | } |
| 167 | return { v: 'correct' }; |
| 168 | } |
| 169 | if (vr.kind === 'diff') { |
| 170 | return allClose(res.values, [vr.value]) ? { v: 'correct' } : { v: 'wrong', note: 'value mismatch' }; |
| 171 | } |
| 172 | if (vr.kind === 'value') { |
| 173 | // Evaluate-to-closed-form: numerically correct AND symbolic (not a bare |
| 174 | // float). A many-digit decimal is a numeric fallback, not an exact result. |
| 175 | if (!allClose(res.values, [vr.value], 1e-9)) return { v: 'wrong', note: 'value mismatch' }; |
| 176 | if (/^[+-]?\d+\.\d{4,}(e[+-]?\d+)?$/i.test(norm(res.text))) |
| 177 | return { v: 'partial', note: 'numeric, not exact' }; |
| 178 | return { v: 'correct' }; |
| 179 | } |
| 180 | if (vr.kind === 'roots') { |
| 181 | // Bijectively match the tool's returned real roots against the reference |
| 182 | // real-root set (order-independent), within a relative tolerance. |
| 183 | const got = (res.values || []).map(Number).filter(Number.isFinite); |
| 184 | const exp = vr.values.map(parseFloat); |
| 185 | const tol = 1e-6; |
| 186 | const used = new Array(got.length).fill(false); |
| 187 | let matched = 0; |
| 188 | for (const e of exp) { |
| 189 | const j = got.findIndex((g, k) => !used[k] && Math.abs(g - e) <= tol * (1 + Math.abs(e))); |
| 190 | if (j >= 0) { used[j] = true; matched++; } |
| 191 | } |
| 192 | const spurious = used.filter((u) => !u).length; |
no test coverage detected